#!/usr/bin/env python # # lineplot.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 LineChart widget. Author: Sven Festersen (sven@sven-festersen.de) """ __docformat__ = "epytext" import gobject import cairo import gtk import math import os 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 RANGE_AUTO = 0 GRAPH_PADDING = 0 #a relative padding GRAPH_POINTS = 1 GRAPH_LINES = 2 GRAPH_BOTH = 3 COLOR_AUTO = 4 POSITION_AUTO = 5 POSITION_LEFT = 6 POSITION_RIGHT = 7 POSITION_BOTTOM = 6 POSITION_TOP = 7 POSITION_TOP_RIGHT = 8 POSITION_BOTTOM_RIGHT = 9 POSITION_BOTTOM_LEFT = 10 POSITION_TOP_LEFT = 11 def draw_point(context, x, y, radius, style): a = radius / 1.414 #1.414=sqrt(2) if style == pygtk_chart.POINT_STYLE_CIRCLE: context.arc(x, y, radius, 0, 2 * math.pi) context.fill() elif style == pygtk_chart.POINT_STYLE_SQUARE: context.rectangle(x - a, y- a, 2 * a, 2 * a) context.fill() elif style == pygtk_chart.POINT_STYLE_CROSS: context.move_to(x, y - a) context.rel_line_to(0, 2 * a) context.stroke() context.move_to(x - a, y) context.rel_line_to(2 * a, 0) context.stroke() elif style == pygtk_chart.POINT_STYLE_TRIANGLE_UP: a = 1.732 * radius #1.732=sqrt(3) b = a / (2 * 1.732) context.move_to(x - a / 2, y + b) context.rel_line_to(a, 0) context.rel_line_to(-a / 2, -(radius + b)) context.rel_line_to(-a / 2, radius + b) context.close_path() context.fill() elif style == pygtk_chart.POINT_STYLE_TRIANGLE_DOWN: a = 1.732 * radius #1.732=sqrt(3) b = a / (2 * 1.732) context.move_to(x - a / 2, y - b) context.rel_line_to(a, 0) context.rel_line_to(-a / 2, radius + b) context.rel_line_to(-a / 2, -(radius + b)) context.close_path() context.fill() elif style == pygtk_chart.POINT_STYLE_DIAMOND: context.move_to(x, y - a) context.rel_line_to(a, a) context.rel_line_to(-a, a) context.rel_line_to(-a, -a) context.rel_line_to(a, -a) context.fill() def draw_point_pixbuf(context, x, y, pixbuf): w = pixbuf.get_width() h = pixbuf.get_height() ax = x - w / 2 ay = y - h / 2 context.set_source_pixbuf(pixbuf, ax, ay) context.rectangle(ax, ay, w, h) context.fill() def draw_errors(context, rect, range_calc, x, y, errors, draw_x, draw_y, xaxis, yaxis, size): if (x, y) in errors: xerror, yerror = errors[(x, y)] if draw_x and xerror > 0: #rect, x, y, xaxis, yaxis left = range_calc.get_absolute_point(rect, x - xerror, y, xaxis, yaxis) right = range_calc.get_absolute_point(rect, x + xerror, y, xaxis, yaxis) context.move_to(left[0], left[1]) context.line_to(right[0], right[1]) context.stroke() context.move_to(left[0], left[1] - size) context.rel_line_to(0, 2 * size) context.stroke() context.move_to(right[0], right[1] - size) context.rel_line_to(0, 2 * size) context.stroke() if draw_y and yerror > 0: top = range_calc.get_absolute_point(rect, x, y - yerror, xaxis, yaxis) bottom = range_calc.get_absolute_point(rect, x, y + yerror, xaxis, yaxis) context.move_to(top[0], top[1]) context.line_to(bottom[0], bottom[1]) context.stroke() context.move_to(top[0] - size, top[1]) context.rel_line_to(2 * size, 0) context.stroke() context.move_to(bottom[0] - size, bottom[1]) context.rel_line_to(2 * size, 0) context.stroke() def separate_data_and_errors(old_data): data = [] errors = {} for d in old_data: if len(d) == 2: data.append(d) elif len(d) == 4: data.append((d[0], d[1])) errors[(d[0], d[1])] = (d[2], d[3]) return data, errors class RangeCalculator: """ This helper class calculates ranges. It is used by the LineChart widget internally, there is no need to create an instance yourself. """ def __init__(self): self._data_xrange = None self._data_yrange = None self._xrange = RANGE_AUTO self._yrange = RANGE_AUTO self._cached_xtics = [] self._cached_ytics = [] def add_graph(self, graph): if self._data_xrange == None: self._data_yrange = graph.get_y_range() self._data_xrange = graph.get_x_range() else: yrange = graph.get_y_range() xrange = graph.get_x_range() if xrange and yrange: xmin = min(xrange[0], self._data_xrange[0]) xmax = max(xrange[1], self._data_xrange[1]) ymin = min(yrange[0], self._data_yrange[0]) ymax = max(yrange[1], self._data_yrange[1]) self._data_xrange = (xmin, xmax) self._data_yrange = (ymin, ymax) def get_ranges(self, xaxis, yaxis): xrange = self._xrange if xrange == RANGE_AUTO: xrange = self._data_xrange if xrange[0] == xrange[1]: xrange = (xrange[0], xrange[0] + 0.1) yrange = self._yrange if yrange == RANGE_AUTO: yrange = self._data_yrange if yrange[0] == yrange[1]: yrange = (yrange[0], yrange[0] + 0.1) if xaxis.get_logarithmic(): xrange = math.log10(xrange[0]), math.log10(xrange[1]) if yaxis.get_logarithmic(): yrange = math.log10(yrange[0]), math.log10(yrange[1]) return (xrange, yrange) def set_xrange(self, xrange): self._xrange = xrange def set_yrange(self, yrange): self._yrange = yrange def get_absolute_zero(self, rect, xaxis, yaxis): xrange, yrange = self.get_ranges(xaxis, yaxis) xfactor = float(rect.width * (1 - 2 * GRAPH_PADDING)) / (xrange[1] - xrange[0]) yfactor = float(rect.height * (1 - 2 * GRAPH_PADDING)) / (yrange[1] - yrange[0]) zx = (rect.width * GRAPH_PADDING) - xrange[0] * xfactor zy = rect.height - ((rect.height * GRAPH_PADDING) - yrange[0] * yfactor) return (zx,zy) def get_absolute_point(self, rect, x, y, xaxis, yaxis): (zx, zy) = self.get_absolute_zero(rect, xaxis, yaxis) xrange, yrange = self.get_ranges(xaxis, yaxis) xfactor = float(rect.width * (1 - 2 * GRAPH_PADDING)) / (xrange[1] - xrange[0]) yfactor = float(rect.height * (1 - 2 * GRAPH_PADDING)) / (yrange[1] - yrange[0]) ax = zx + x * xfactor ay = zy - y * yfactor return (ax, ay) def prepare_tics(self, rect, xaxis, yaxis): self._cached_xtics = self._get_xtics(rect, xaxis, yaxis) self._cached_ytics = self._get_ytics(rect, xaxis, yaxis) def get_xtics(self, rect): return self._cached_xtics def get_ytics(self, rect): return self._cached_ytics def _get_xtics(self, rect, xaxis, yaxis): tics = [] (zx, zy) = self.get_absolute_zero(rect, xaxis, yaxis) (xrange, yrange) = self.get_ranges(xaxis, yaxis) delta = xrange[1] - xrange[0] exp = int(math.log10(delta)) - 1 first_n = int(xrange[0] / (10 ** exp)) last_n = int(xrange[1] / (10 ** exp)) n = last_n - first_n N = rect.width / 50.0 divide_by = int(n / N) if divide_by == 0: divide_by = 1 left = rect.width * GRAPH_PADDING right = rect.width * (1 - GRAPH_PADDING) for i in range(first_n, last_n + 1): num = i * 10 ** exp (x, y) = self.get_absolute_point(rect, num, 0, xaxis, yaxis) if i % divide_by == 0 and is_in_range(x, (left, right)): tics.append(((x, y), num)) return tics def _get_ytics(self, rect, xaxis, yaxis): tics = [] (zx, zy) = self.get_absolute_zero(rect, xaxis, yaxis) (xrange, yrange) = self.get_ranges(xaxis, yaxis) delta = yrange[1] - yrange[0] exp = int(math.log10(delta)) - 1 first_n = int(yrange[0] / (10 ** exp)) last_n = int(yrange[1] / (10 ** exp)) n = last_n - first_n N = rect.height / 50.0 divide_by = int(n / N) if divide_by == 0: divide_by = 1 top = rect.height * GRAPH_PADDING bottom = rect.height * (1 - GRAPH_PADDING) for i in range(first_n, last_n + 1): num = i * 10 ** exp (x, y) = self.get_absolute_point(rect, 0, num, xaxis, yaxis) if i % divide_by == 0 and is_in_range(y, (top, bottom)): tics.append(((x, y), num)) return tics class LineChart(chart.Chart): """ A widget that shows a line chart. The following attributes can be accessed: - LineChart.background (inherited from chart.Chart) - LineChart.title (inherited from chart.Chart) - LineChart.graphs (a dict that holds the graphs identified by their name) - LineChart.grid - LineChart.xaxis - LineChart.yaxis Properties ========== LineChart inherits properties from chart.Chart. Signals ======= The LineChart class inherits signals from chart.Chart. Additional chart: - datapoint-clicked (emitted if a datapoint is clicked) - datapoint-hovered (emitted if a datapoint is hovered with the mouse pointer) Callback signature for both signals: def callback(linechart, graph, (x, y)) """ __gsignals__ = {"datapoint-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)), "datapoint-hovered": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT))} def __init__(self): chart.Chart.__init__(self) self.graphs = {} self._range_calc = RangeCalculator() self.xaxis = XAxis(self._range_calc) self.yaxis = YAxis(self._range_calc) self.grid = Grid(self._range_calc) self.legend = Legend() self._highlighted_points = [] self.xaxis.connect("appearance_changed", self._cb_appearance_changed) self.yaxis.connect("appearance_changed", self._cb_appearance_changed) self.grid.connect("appearance_changed", self._cb_appearance_changed) self.legend.connect("appearance_changed", self._cb_appearance_changed) def __iter__(self): for name, graph in self.graphs.iteritems(): yield graph def _cb_button_pressed(self, widget, event): points = chart.get_sensitive_areas(event.x, event.y) for x, y, graph in points: self.emit("datapoint-clicked", graph, (x, y)) def _cb_motion_notify(self, widget, event): self._highlighted_points = chart.get_sensitive_areas(event.x, event.y) for x, y, graph in self._highlighted_points: self.emit("datapoint-hovered", graph, (x, y)) self.queue_draw() def _do_draw_graphs(self, context, rect): """ Draw all the graphs. @type context: cairo.Context @param context: The context to draw on. @type rect: gtk.gdk.Rectangle @param rect: A rectangle representing the charts area. """ for (name, graph) in self.graphs.iteritems(): graph.draw(context, rect, self.xaxis, self.yaxis, self._highlighted_points) self._highlighted_points = [] def _do_draw_axes(self, context, rect): """ Draw x and y axis. @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.xaxis.draw(context, rect, self.yaxis) self.yaxis.draw(context, rect, self.xaxis) 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() ############### graph_rect = rect graph_rect.y += 10 graph_rect.height -= 26 # Make the thing bigger if (self.legend.get_property("visible") == True) and (self.legend.get_position() == POSITION_RIGHT): graph_rect.width -= self.legend.last_width self._range_calc.prepare_tics(graph_rect, self.xaxis, self.yaxis) #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) data_available = False for (name, graph) in self.graphs.iteritems(): if graph.has_something_to_draw(): data_available = True break if self.graphs and data_available: self.grid.draw(context, graph_rect, self.xaxis, self.yaxis) # This is the grid self._do_draw_axes(context, graph_rect) self._do_draw_graphs(context, graph_rect) label.finish_drawing() self.legend.draw(context, rect, self.graphs) def add_graph(self, graph): """ Add a graph object to the plot. @type graph: line_chart.Graph @param graph: The graph to add. """ if graph.get_color() == COLOR_AUTO: graph.set_color(COLORS[len(self.graphs) % len(COLORS)]) graph.set_range_calc(self._range_calc) self.graphs[graph.get_name()] = graph self._range_calc.add_graph(graph) graph.connect("appearance-changed", self._cb_appearance_changed) def remove_graph(self, name): """ Remove a graph from the plot. @type name: string @param name: The name of the graph to remove. """ del self.graphs[name] self.queue_draw() def set_xrange(self, xrange): """ Set the visible xrange. xrange has to be a pair: (xmin, xmax) or RANGE_AUTO. If you set it to RANGE_AUTO, the visible range will be calculated. @type xrange: pair of numbers @param xrange: The new xrange. """ self._range_calc.set_xrange(xrange) self.queue_draw() def get_xrange(self): return self._range_calc.get_ranges(self.xaxis, self.yaxis)[0] def set_yrange(self, yrange): """ Set the visible yrange. yrange has to be a pair: (ymin, ymax) or RANGE_AUTO. If you set it to RANGE_AUTO, the visible range will be calculated. @type yrange: pair of numbers @param yrange: The new yrange. """ self._range_calc.set_yrange(yrange) self.queue_draw() def get_yrange(self): return self._range_calc.get_ranges(self.xaxis, self.yaxis)[1] class Axis(ChartObject): """ This class represents an axis on the line chart. Properties ========== The Axis class inherits properties from chart_object.ChartObject. Additional properties: - label (a label for the axis, type: string) - show-label (sets whether the axis' label should be shown, type: boolean) - position (position of the axis, type: an axis position constant) - show-tics (sets whether tics should be shown at the axis, type: boolean) - show-tic-lables (sets whether labels should be shown at the tics, type: boolean) - tic-format-function (a function that is used to format the tic labels, default: str) - logarithmic (sets whether the axis should use a logarithmic scale, type: boolean). Signals ======= The Axis class inherits signals from chart_object.ChartObject. """ __gproperties__ = {"label": (gobject.TYPE_STRING, "axis label", "The label of the axis.", "", gobject.PARAM_READWRITE), "show-label": (gobject.TYPE_BOOLEAN, "show label", "Set whether to show the axis label.", True, gobject.PARAM_READWRITE), "position": (gobject.TYPE_INT, "axis position", "Position of the axis.", 5, 7, 5, gobject.PARAM_READWRITE), "show-tics": (gobject.TYPE_BOOLEAN, "show tics", "Set whether to draw tics.", True, gobject.PARAM_READWRITE), "show-tic-labels": (gobject.TYPE_BOOLEAN, "show tic labels", "Set whether to draw tic labels", True, gobject.PARAM_READWRITE), "tic-format-function": (gobject.TYPE_PYOBJECT, "tic format function", "This function is used to label the tics.", gobject.PARAM_READWRITE), "logarithmic": (gobject.TYPE_BOOLEAN, "logarithmic scale", "Set whether to use logarithmic scale.", False, gobject.PARAM_READWRITE)} def __init__(self, range_calc, label): ChartObject.__init__(self) self.set_property("antialias", False) self._label = label self._show_label = True self._position = POSITION_AUTO self._show_tics = True self._show_tic_labels = True self._tic_format_function = str self._logarithmic = False self._range_calc = range_calc def do_get_property(self, property): if property.name == "visible": return self._show elif property.name == "antialias": return self._antialias elif property.name == "label": return self._label elif property.name == "show-label": return self._show_label elif property.name == "position": return self._position elif property.name == "show-tics": return self._show_tics elif property.name == "show-tic-labels": return self._show_tic_labels elif property.name == "tic-format-function": return self._tic_format_function elif property.name == "logarithmic": return self._logarithmic 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 == "label": self._label = value elif property.name == "show-label": self._show_label = value elif property.name == "position": self._position = value elif property.name == "show-tics": self._show_tics = value elif property.name == "show-tic-labels": self._show_tic_labels = value elif property.name == "tic-format-function": self._tic_format_function = value elif property.name == "logarithmic": self._logarithmic = value else: raise AttributeError, "Property %s does not exist." % property.name def set_label(self, label): """ Set the label of the axis. @param label: new label @type label: string. """ self.set_property("label", label) self.emit("appearance_changed") def get_label(self): """ Returns the current label of the axis. @return: string. """ return self.get_property("label") def set_show_label(self, show): """ Set whether to show the axis' label. @type show: boolean. """ self.set_property("show-label", show) self.emit("appearance_changed") def get_show_label(self): """ Returns True if the axis' label is shown. @return: boolean. """ return self.get_property("show-label") def set_position(self, pos): """ Set the position of the axis. pos hast to be one these constants: POSITION_AUTO, POSITION_BOTTOM, POSITION_LEFT, POSITION_RIGHT, POSITION_TOP. """ self.set_property("position", pos) self.emit("appearance_changed") def get_position(self): """ Returns the position of the axis. (see set_position for details). """ return self.get_property("position") def set_show_tics(self, show): """ Set whether to draw tics at the axis. @type show: boolean. """ self.set_property("show-tics", show) self.emit("appearance_changed") def get_show_tics(self): """ Returns True if tics are drawn. @return: boolean. """ return self.get_property("show-tics") def set_show_tic_labels(self, show): """ Set whether to draw tic labels. Labels are only drawn if tics are drawn. @type show: boolean. """ self.set_property("show-tic-labels", show) self.emit("appearance_changed") def get_show_tic_labels(self): """ Returns True if tic labels are shown. @return: boolean. """ return self.get_property("show-tic-labels") def set_tic_format_function(self, func): """ Use this to set the function that should be used to label the tics. The function should take a number as the only argument and return a string. Default: str @type func: function. """ self.set_property("tic-format-function", func) self.emit("appearance_changed") def get_tic_format_function(self): """ Returns the function currently used for labeling the tics. """ return self.get_property("tic-format-function") def set_logarithmic(self, log): """ Set whether the axis should use logarithmic (base 10) scale. @type log: boolean. """ self.set_property("logarithmic", log) self.emit("appearance_changed") def get_logarithmic(self): """ Returns True if the axis uses logarithmic scale. @return: boolean. """ return self.get_property("logarithmic") class XAxis(Axis): """ This class represents the xaxis. It is used by the LineChart widget internally, there is no need to create an instance yourself. Properties ========== The XAxis class inherits properties from Axis. Signals ======= The XAxis class inherits signals from Axis. """ def __init__(self, range_calc): Axis.__init__(self, range_calc, "x") def draw(self, context, rect, yaxis): """ This method is called by the parent Plot instance. It calls _do_draw. """ if self._show: if not self._antialias: context.set_antialias(cairo.ANTIALIAS_NONE) self._do_draw(context, rect, yaxis) context.set_antialias(cairo.ANTIALIAS_DEFAULT) def _do_draw_tics(self, context, rect, yaxis): if self._show_tics: tics = self._range_calc.get_xtics(rect) #calculate yaxis position (zx, zy) = self._range_calc.get_absolute_zero(rect, self, yaxis) if yaxis.get_position() == POSITION_LEFT: zx = rect.width * GRAPH_PADDING elif yaxis.get_position() == POSITION_RIGHT: zx = rect.width * (1 - GRAPH_PADDING) for ((x,y), val) in tics: if self._position == POSITION_TOP: y = rect.height * GRAPH_PADDING elif self._position == POSITION_BOTTOM: y = rect.height * (1 - GRAPH_PADDING) tic_height = rect.height / 80.0 context.move_to(x, y + tic_height / 2) context.rel_line_to(0, - tic_height) context.stroke() if self._show_tic_labels: if abs(x - zx) < 10: #the distance to the yaxis is to small => do not draw label continue pos = x, y + tic_height text = self._tic_format_function(val) tic_label = label.Label(pos, text, anchor=label.ANCHOR_TOP_CENTER, fixed=True) tic_label.draw(context, rect) def _do_draw_label(self, context, rect, pos): axis_label = label.Label(pos, self._label, anchor=label.ANCHOR_LEFT_CENTER, fixed=True) axis_label.draw(context, rect) def _do_draw(self, context, rect, yaxis): """ Draw the axis. """ (zx, zy) = self._range_calc.get_absolute_zero(rect, self, yaxis) if self._position == POSITION_BOTTOM: zy = rect.height * (1 - GRAPH_PADDING) elif self._position == POSITION_TOP: zy = rect.height * GRAPH_PADDING if rect.height * GRAPH_PADDING <= zy and rect.height * (1 - GRAPH_PADDING) >= zy: context.set_source_rgb(0, 0, 0) #draw the line: context.move_to(rect.width * GRAPH_PADDING, zy) context.line_to(rect.width * (1 - GRAPH_PADDING), zy) context.stroke() #draw arrow: context.move_to(rect.width * (1 - GRAPH_PADDING) + 3, zy) context.rel_line_to(-3, -3) context.rel_line_to(0, 6) context.close_path() context.fill() if self._show_label: self._do_draw_label(context, rect, (rect.width * (1 - GRAPH_PADDING) + 3, zy)) self._do_draw_tics(context, rect, yaxis) class YAxis(Axis): """ This class represents the yaxis. It is used by the LineChart widget internally, there is no need to create an instance yourself. Properties ========== The YAxis class inherits properties from Axis. Signals ======= The YAxis class inherits signals from Axis. """ def __init__(self, range_calc): Axis.__init__(self, range_calc, "y") def draw(self, context, rect, xaxis): """ This method is called by the parent Plot instance. It calls _do_draw. """ if self._show: if not self._antialias: context.set_antialias(cairo.ANTIALIAS_NONE) self._do_draw(context, rect, xaxis) context.set_antialias(cairo.ANTIALIAS_DEFAULT) def _do_draw_tics(self, context, rect, xaxis): if self._show_tics: tics = self._range_calc.get_ytics(rect) #calculate xaxis position (zx, zy) = self._range_calc.get_absolute_zero(rect, xaxis, self) if xaxis.get_position() == POSITION_BOTTOM: zy = rect.height * (1 - GRAPH_PADDING) elif xaxis.get_position() == POSITION_TOP: zy = rect.height * GRAPH_PADDING for ((x,y), val) in tics: if self._position == POSITION_LEFT: x = rect.width * GRAPH_PADDING elif self._position == POSITION_RIGHT: x = rect.width * (1 - GRAPH_PADDING) tic_width = rect.height / 80.0 context.move_to(x + tic_width / 2, y) context.rel_line_to(- tic_width, 0) context.stroke() if self._show_tic_labels: if abs(y - zy) < 10: #distance to xaxis is to small => do not draw label continue pos = x - tic_width, y text = self._tic_format_function(val) tic_label = label.Label(pos, text, anchor=label.ANCHOR_RIGHT_CENTER, fixed=True) tic_label.draw(context, rect) def _do_draw_label(self, context, rect, pos): axis_label = label.Label(pos, self._label, anchor=label.ANCHOR_BOTTOM_CENTER, fixed=True) axis_label.draw(context, rect) def _do_draw(self, context, rect, xaxis): (zx, zy) = self._range_calc.get_absolute_zero(rect, xaxis, self) if self._position == POSITION_LEFT: zx = rect.width * GRAPH_PADDING elif self._position == POSITION_RIGHT: zx = rect.width * (1 - GRAPH_PADDING) if rect.width * GRAPH_PADDING <= zx and rect.width * (1 - GRAPH_PADDING) >= zx: context.set_source_rgb(0, 0, 0) #draw line: context.move_to(zx, rect.height * (1 - GRAPH_PADDING)) context.line_to(zx, rect.height * GRAPH_PADDING) context.stroke() #draw arrow: context.move_to(zx, rect.height * GRAPH_PADDING - 3) context.rel_line_to(-3, 3) context.rel_line_to(6, 0) context.close_path() context.fill() if self._show_label: self._do_draw_label(context, rect, (zx, rect.height * GRAPH_PADDING - 3)) self._do_draw_tics(context, rect, xaxis) class Grid(ChartObject): """ A class representing the grid of the chart. It is used by the LineChart widget internally, there is no need to create an instance yourself. Properties ========== The Grid class inherits properties from chart_object.ChartObject. Additional properties: - show-horizontal (sets whther to show horizontal grid lines, type: boolean) - show-vertical (sets whther to show vertical grid lines, type: boolean) - color (the color of the grid lines, type: gtk.gdk.Color) - line-style-horizontal (the line style of the horizontal grid lines, type: a line style constant) - line-style-vertical (the line style of the vertical grid lines, type: a line style constant). Signals ======= The Grid class inherits signals from chart_object.ChartObject. """ __gproperties__ = {"show-horizontal": (gobject.TYPE_BOOLEAN, "show horizontal lines", "Set whether to draw horizontal lines.", True, gobject.PARAM_READWRITE), "show-vertical": (gobject.TYPE_BOOLEAN, "show vertical lines", "Set whether to draw vertical lines.", True, gobject.PARAM_READWRITE), "color": (gobject.TYPE_PYOBJECT, "grid color", "The color of the grid in (r,g,b) format. r,g,b in [0,1]", gobject.PARAM_READWRITE), "line-style-horizontal": (gobject.TYPE_INT, "horizontal line style", "Line Style for the horizontal grid lines", 0, 3, 0, gobject.PARAM_READWRITE), "line-style-vertical": (gobject.TYPE_INT, "vertical line style", "Line Style for the vertical grid lines", 0, 3, 0, gobject.PARAM_READWRITE)} def __init__(self, range_calc): ChartObject.__init__(self) self.set_property("antialias", False) self._range_calc = range_calc self._color = gtk.gdk.color_parse("#DEDEDE") self._show_h = True self._show_v = True self._line_style_h = pygtk_chart.LINE_STYLE_SOLID self._line_style_v = pygtk_chart.LINE_STYLE_SOLID def do_get_property(self, property): if property.name == "visible": return self._show elif property.name == "antialias": return self._antialias elif property.name == "show-horizontal": return self._show_h elif property.name == "show-vertical": return self._show_v elif property.name == "color": return self._color elif property.name == "line-style-horizontal": return self._line_style_h elif property.name == "line-style-vertical": return self._line_style_v 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-horizontal": self._show_h = value elif property.name == "show-vertical": self._show_v = value elif property.name == "color": self._color = value elif property.name == "line-style-horizontal": self._line_style_h = value elif property.name == "line-style-vertical": self._line_style_v = value else: raise AttributeError, "Property %s does not exist." % property.name def _do_draw(self, context, rect, xaxis, yaxis): context.set_source_rgb(*color_gdk_to_cairo(self._color)) #draw horizontal lines if self._show_h: set_context_line_style(context, self._line_style_h) ytics = self._range_calc.get_ytics(rect) xa = rect.width * GRAPH_PADDING xb = rect.width * (1 - GRAPH_PADDING) for ((x, y), label) in ytics: context.move_to(xa, y) context.line_to(xb, y) context.stroke() context.set_dash([]) #draw vertical lines if self._show_v: set_context_line_style(context, self._line_style_v) xtics = self._range_calc.get_xtics(rect) ya = rect.height * GRAPH_PADDING yb = rect.height * (1 - GRAPH_PADDING) for ((x, y), label) in xtics: context.move_to(x, ya) context.line_to(x, yb) context.stroke() context.set_dash([]) def set_draw_horizontal_lines(self, draw): """ Set whether to draw horizontal grid lines. @type draw: boolean. """ self.set_property("show-horizontal", draw) self.emit("appearance_changed") def get_draw_horizontal_lines(self): """ Returns True if horizontal grid lines are drawn. @return: boolean. """ return self.get_property("show-horizontal") def set_draw_vertical_lines(self, draw): """ Set whether to draw vertical grid lines. @type draw: boolean. """ self.set_property("show-vertical", draw) self.emit("appearance_changed") def get_draw_vertical_lines(self): """ Returns True if vertical grid lines are drawn. @return: boolean. """ return self.get_property("show-vertical") def set_color(self, color): """ Set the color of the grid. @type color: gtk.gdk.Color @param color: The new color of the grid. """ self.set_property("color", color) self.emit("appearance_changed") def get_color(self): """ Returns the color of the grid. @return: gtk.gdk.Color. """ return self.get_property("color") def set_line_style_horizontal(self, style): """ Set the line style of the horizontal grid lines. style has to be one of these constants: - 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-horizontal", style) self.emit("appearance_changed") def get_line_style_horizontal(self): """ Returns ths current horizontal line style. @return: a line style constant. """ return self.get_property("line-style-horizontal") def set_line_style_vertical(self, style): """ Set the line style of the vertical grid lines. style has to be one of these constants: - 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-vertical", style) self.emit("appearance_changed") def get_line_style_vertical(self): """ Returns ths current vertical line style. @return: a line style constant. """ return self.get_property("line-style-vertical") class Graph(ChartObject): """ This class represents a graph or the data you want to plot on your LineChart widget. Properties ========== The Graph class inherits properties from chart_object.ChartObject. Additional properties: - name (a unique id for the graph, type: string, read only) - title (the graph's title, type: string) - color (the graph's color, type: gtk.gdk.Color) - type (graph type, type: a graph type constant) - point-size (radius of the datapoints in px, type: int in [1, 100]) - fill-to (set how to fill space under the graph, type: None, Graph or float) - fill-color (the color of the filling, type: gtk.gdk.Color) - fill-opacity (the opacity of the filling, type: float in [0, 1]) - show-values (sets whether y values should be shown at the datapoints, type: boolean) - show-title (sets whether ot show the graph's title, type: boolean) - line-style (the graph's line style, type: a line style constant) - point-style (the graph's datapoints' point style, type: a point style constant) - clickable (sets whether datapoints are sensitive for clicks, type: boolean) - show-xerrors (sets whether x errors should be shown if error data is available, type: boolean) - show-yerrors (sets whether y errors should be shown if error data is available, type: boolean). Signals ======= The Graph class inherits signals from chart_object.ChartObject. """ __gproperties__ = {"name": (gobject.TYPE_STRING, "graph id", "The graph's unique name.", "", gobject.PARAM_READABLE), "title": (gobject.TYPE_STRING, "graph title", "The title of the graph.", "", gobject.PARAM_READWRITE), "color": (gobject.TYPE_PYOBJECT, "graph color", "The color of the graph in (r,g,b) format. r,g,b in [0,1].", gobject.PARAM_READWRITE), "type": (gobject.TYPE_INT, "graph type", "The type of the graph.", 1, 3, 3, gobject.PARAM_READWRITE), "point-size": (gobject.TYPE_INT, "point size", "Radius of the data points.", 1, 100, 2, gobject.PARAM_READWRITE), "fill-to": (gobject.TYPE_PYOBJECT, "fill to", "Set how to fill space under the graph.", gobject.PARAM_READWRITE), "fill-color": (gobject.TYPE_PYOBJECT, "fill color", "Set which color to use when filling space under the graph.", gobject.PARAM_READWRITE), "fill-opacity": (gobject.TYPE_FLOAT, "fill opacity", "Set which opacity to use when filling space under the graph.", 0.0, 1.0, 0.3, gobject.PARAM_READWRITE), "show-values": (gobject.TYPE_BOOLEAN, "show values", "Sets whether to show the y values.", False, gobject.PARAM_READWRITE), "show-title": (gobject.TYPE_BOOLEAN, "show title", "Sets whether to show the graph's title.", True, gobject.PARAM_READWRITE), "line-style": (gobject.TYPE_INT, "line style", "The line style to use.", 0, 3, 0, gobject.PARAM_READWRITE), "point-style": (gobject.TYPE_PYOBJECT, "point style", "The graph's point style.", gobject.PARAM_READWRITE), "clickable": (gobject.TYPE_BOOLEAN, "clickable", "Sets whether datapoints should be clickable.", True, gobject.PARAM_READWRITE), "show-xerrors": (gobject.TYPE_BOOLEAN, "show xerrors", "Set whether to show x-errorbars.", True, gobject.PARAM_READWRITE), "show-yerrors": (gobject.TYPE_BOOLEAN, "show yerrors", "Set whether to show y-errorbars.", True, gobject.PARAM_READWRITE)} def __init__(self, name, title, data): """ Create a new graph instance. data should be a list of x,y pairs. If you want to provide error data for a datapoint, the tuple for that point has to be (x, y, xerror, yerror). If you want only one error, set the other to zero. You can mix datapoints with and without error data in data. @type name: string @param name: A unique name for the graph. This could be everything. It's just a name used internally for identification. You need to know this if you want to access or delete a graph from a chart. @type title: string @param title: The graphs title. This can be drawn on the chart. @type data: list (see above) @param data: This is the data you want to be visualized. For detail see description above. """ ChartObject.__init__(self) self._name = name self._title = title self._data, self._errors = separate_data_and_errors(data) self._color = COLOR_AUTO self._type = GRAPH_BOTH self._point_size = 2 self._show_value = False self._show_title = True self._fill_to = None self._fill_color = COLOR_AUTO self._fill_opacity = 0.3 self._line_style = pygtk_chart.LINE_STYLE_SOLID self._point_style = pygtk_chart.POINT_STYLE_CIRCLE self._clickable = True self._draw_xerrors = True self._draw_yerrors = True self._range_calc = None self._label = label.Label((0, 0), self._title, anchor=label.ANCHOR_LEFT_CENTER) 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 == "color": return self._color elif property.name == "type": return self._type elif property.name == "point-size": return self._point_size elif property.name == "fill-to": return self._fill_to elif property.name == "fill-color": return self._fill_color elif property.name == "fill-opacity": return self._fill_opacity elif property.name == "show-values": return self._show_value elif property.name == "show-title": return self._show_title elif property.name == "line-style": return self._line_style elif property.name == "point-style": return self._point_style elif property.name == "clickable": return self._clickable elif property.name == "show-xerrors": return self._draw_xerrors elif property.name == "show-yerrors": return self._draw_yerrors 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 == "title": self._label.set_text(value) self._title = value elif property.name == "color": self._color = value elif property.name == "type": self._type = value elif property.name == "point-size": self._point_size = value elif property.name == "fill-to": self._fill_to = value elif property.name == "fill-color": self._fill_color = value elif property.name == "fill-opacity": self._fill_opacity = value elif property.name == "show-values": self._show_value = value elif property.name == "show-title": self._show_title = value elif property.name == "line-style": self._line_style = value elif property.name == "point-style": self._point_style = value elif property.name == "clickable": self._clickable = value elif property.name == "show-xerrors": self._draw_xerrors = value elif property.name == "show-yerrors": self._draw_yerrors = value else: raise AttributeError, "Property %s does not exist." % property.name def has_something_to_draw(self): return self._data != [] def _do_draw_lines(self, context, rect, xrange, yrange, xaxis, yaxis): context.set_source_rgb(*color_gdk_to_cairo(self._color)) set_context_line_style(context, self._line_style) first_point = None last_point = None for (x, y) in self._data: if xaxis.get_logarithmic(): x = math.log10(x) if yaxis.get_logarithmic(): y = math.log10(y) if is_in_range(x, xrange) and is_in_range(y, yrange): (ax, ay) = self._range_calc.get_absolute_point(rect, x, y, xaxis, yaxis) if first_point == None: context.move_to(ax, ay) first_point = x, y else: context.line_to(ax, ay) last_point = ax, ay context.stroke() context.set_dash([]) return first_point, last_point def _do_draw_points(self, context, rect, xrange, yrange, xaxis, yaxis, highlighted_points): context.set_source_rgb(*color_gdk_to_cairo(self._color)) first_point = None last_point = None for (x, y) in self._data: if xaxis.get_logarithmic(): x = math.log10(x) if yaxis.get_logarithmic(): y = math.log10(y) if is_in_range(x, xrange) and is_in_range(y, yrange): (ax, ay) = self._range_calc.get_absolute_point(rect, x, y, xaxis, yaxis) if self._clickable: chart.add_sensitive_area(chart.AREA_CIRCLE, (ax, ay, self._point_size), (x, y, self)) if first_point == None: context.move_to(ax, ay) #draw errors draw_errors(context, rect, self._range_calc, x, y, self._errors, self._draw_xerrors, self._draw_yerrors, xaxis, yaxis, self._point_size) #draw the point if type(self._point_style) != gtk.gdk.Pixbuf: draw_point(context, ax, ay, self._point_size, self._point_style) highlighted = (x, y, self) in highlighted_points if highlighted and self._clickable: context.set_source_rgba(1, 1, 1, 0.3) draw_point(context, ax, ay, self._point_size, self._point_style) context.set_source_rgb(*color_gdk_to_cairo(self._color)) else: draw_point_pixbuf(context, ax, ay, self._point_style) last_point = ax, ay return first_point, last_point def _do_draw_values(self, context, rect, xrange, yrange, xaxis, yaxis): anchors = {} first_point = True for i, (x, y) in enumerate(self._data): if xaxis.get_logarithmic(): x = math.log10(x) if yaxis.get_logarithmic(): y = math.log10(y) if is_in_range(x, xrange) and is_in_range(y, yrange): next_point = None if i + 1 < len(self._data) and (is_in_range(self._data[i + 1][0], xrange) and is_in_range(self._data[i + 1][1], yrange)): next_point = self._data[i + 1] if first_point: if next_point != None: if next_point[1] >= y: anchors[(x, y)] = label.ANCHOR_TOP_LEFT else: anchors[(x, y)] = label.ANCHOR_BOTTOM_LEFT first_point = False else: previous_point = self._data[i - 1] if next_point != None: if previous_point[1] <= y <= next_point[1]: anchors[(x, y)] = label.ANCHOR_BOTTOM_RIGHT elif previous_point[1] > y > next_point[1]: anchors[(x, y)] = label.ANCHOR_BOTTOM_LEFT elif previous_point[1] < y and next_point[1] < y: anchors[(x, y)] = label.ANCHOR_BOTTOM_CENTER elif previous_point[1] > y and next_point[1] > y: anchors[(x, y)] = label.ANCHOR_TOP_CENTER else: if previous_point[1] >= y: anchors[(x, y)] = label.ANCHOR_TOP_RIGHT else: anchors[(x, y)] = label.ANCHOR_BOTTOM_RIGHT for x, y in self._data: if xaxis.get_logarithmic(): x = math.log10(x) if yaxis.get_logarithmic(): y = math.log10(y) if (x, y) in anchors and is_in_range(x, xrange) and is_in_range(y, yrange): (ax, ay) = self._range_calc.get_absolute_point(rect, x, y, xaxis, yaxis) value_label = label.Label((ax, ay), str(y), anchor=anchors[(x, y)]) value_label.set_color(self._color) value_label.draw(context, rect) def _do_draw_title(self, context, rect, last_point, xaxis, yaxis): """ Draws the 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. @type last_point: pairs of numbers @param last_point: The absolute position of the last drawn data point. """ if last_point: x = last_point[0] + 5 y = last_point[1] self._label.set_position((x, y)) self._label.set_color(self._color) self._label.draw(context, rect) def _do_draw_fill(self, context, rect, xrange, xaxis, yaxis): if type(self._fill_to) in (int, float): data = [] for i, (x, y) in enumerate(self._data): if xaxis.get_logarithmic(): x = math.log10(x) if yaxis.get_logarithmic(): y = math.log10(y) if is_in_range(x, xrange) and not data: data.append((x, self._fill_to)) elif not is_in_range(x, xrange) and len(data) == 1: data.append((prev, self._fill_to)) break elif i == len(self._data) - 1: data.append((x, self._fill_to)) prev = x graph = Graph("none", "", data) elif type(self._fill_to) == Graph: graph = self._fill_to d = graph.get_data() range_b = d[0][0], d[-1][0] xrange = intersect_ranges(xrange, range_b) if not graph.get_visible(): return c = self._fill_color if c == COLOR_AUTO: c = self._color c = color_gdk_to_cairo(c) context.set_source_rgba(c[0], c[1], c[2], self._fill_opacity) data_a = self._data data_b = graph.get_data() first = True start_point = (0, 0) for x, y in data_a: if xaxis.get_logarithmic(): x = math.log10(x) if yaxis.get_logarithmic(): y = math.log10(y) if is_in_range(x, xrange): xa, ya = self._range_calc.get_absolute_point(rect, x, y, xaxis, yaxis) if first: context.move_to(xa, ya) start_point = xa, ya first = False else: context.line_to(xa, ya) first = True for i in range(0, len(data_b)): j = len(data_b) - i - 1 x, y = data_b[j] xa, ya = self._range_calc.get_absolute_point(rect, x, y, xaxis, yaxis) if is_in_range(x, xrange): context.line_to(xa, ya) context.line_to(*start_point) context.fill() def _do_draw(self, context, rect, xaxis, yaxis, highlighted_points): """ Draw the graph. @type context: cairo.Context @param context: The context to draw on. @type rect: gtk.gdk.Rectangle @param rect: A rectangle representing the charts area. """ (xrange, yrange) = self._range_calc.get_ranges(xaxis, yaxis) if self._type in [GRAPH_LINES, GRAPH_BOTH]: first_point, last_point = self._do_draw_lines(context, rect, xrange, yrange, xaxis, yaxis) if self._type in [GRAPH_POINTS, GRAPH_BOTH]: first_point, last_point = self._do_draw_points(context, rect, xrange, yrange, xaxis, yaxis, highlighted_points) if self._fill_to != None: self._do_draw_fill(context, rect, xrange, xaxis, yaxis) if self._show_value and self._type in [GRAPH_POINTS, GRAPH_BOTH]: self._do_draw_values(context, rect, xrange, yrange, xaxis, yaxis) if self._show_title: self._do_draw_title(context, rect, last_point, xaxis, yaxis) def get_x_range(self): """ Get the the endpoints of the x interval. @return: pair of numbers """ try: self._data.sort(lambda x, y: cmp(x[0], y[0])) return (self._data[0][0], self._data[-1][0]) except: return None def get_y_range(self): """ Get the the endpoints of the y interval. @return: pair of numbers """ try: self._data.sort(lambda x, y: cmp(x[1], y[1])) return (self._data[0][1], self._data[-1][1]) except: return None def get_name(self): """ Get the name of the graph. @return: string """ return self.get_property("name") def get_title(self): """ Returns the title of the graph. @return: string """ return self.get_property("title") def set_title(self, title): """ Set the title of the graph. @type title: string @param title: The graph's new title. """ self.set_property("title", title) self.emit("appearance_changed") def set_range_calc(self, range_calc): self._range_calc = range_calc def get_color(self): """ Returns the current color of the graph or COLOR_AUTO. @return: gtk.gdk.Color or COLOR_AUTO. """ return self.get_property("color") def set_color(self, color): """ Set the color of the graph. If set to COLOR_AUTO, the color will be choosen dynamicly. @type color: gtk.gdk.Color @param color: The new color of the graph. """ self.set_property("color", color) self.emit("appearance_changed") def get_type(self): """ Returns the type of the graph. @return: a type constant (see set_type() for details) """ return self.get_property("type") def set_type(self, type): """ Set the type of the graph to one of these: - GRAPH_POINTS: only show points - GRAPH_LINES: only draw lines - GRAPH_BOTH: draw points and lines, i.e. connect points with lines @param type: One of the constants above. """ self.set_property("type", type) self.emit("appearance_changed") def get_point_size(self): """ Returns the radius of the data points. @return: a poisitive integer """ return self.get_property("point_size") def set_point_size(self, size): """ Set the radius of the drawn points. @type size: a positive integer in [1, 100] @param size: The new radius of the points. """ self.set_property("point_size", size) self.emit("appearance_changed") def get_fill_to(self): """ The return value of this method depends on the filling under the graph. See set_fill_to() for details. """ return self.get_property("fill-to") def set_fill_to(self, fill_to): """ Use this method to specify how the space under the graph should be filled. fill_to has to be one of these: - None: dont't fill the space under the graph. - int or float: fill the space to the value specified (setting fill_to=0 means filling the space between graph and xaxis). - a Graph object: fill the space between this graph and the graph given as the argument. The color of the filling is the graph's color with 30% opacity. @type fill_to: one of the possibilities listed above. """ self.set_property("fill-to", fill_to) self.emit("appearance_changed") def get_fill_color(self): """ Returns the color that is used to fill space under the graph or COLOR_AUTO. @return: gtk.gdk.Color or COLOR_AUTO. """ return self.get_property("fill-color") def set_fill_color(self, color): """ Set which color should be used when filling the space under a graph. If color is COLOR_AUTO, the graph's color will be used. @type color: gtk.gdk.Color or COLOR_AUTO. """ self.set_property("fill-color", color) self.emit("appearance_changed") def get_fill_opacity(self): """ Returns the opacity that is used to fill space under the graph. """ return self.get_property("fill-opacity") def set_fill_opacity(self, opacity): """ Set which opacity should be used when filling the space under a graph. The default is 0.3. @type opacity: float in [0, 1]. """ self.set_property("fill-opacity", opacity) self.emit("appearance_changed") def get_show_values(self): """ Returns True if y values are shown. @return: boolean """ return self.get_property("show-values") def set_show_values(self, show): """ Set whether the y values should be shown (only if graph type is GRAPH_POINTS or GRAPH_BOTH). @type show: boolean """ self.set_property("show-values", show) self.emit("appearance_changed") def get_show_title(self): """ Returns True if the title of the graph is shown. @return: boolean. """ return self.get_property("show-title") def set_show_title(self, show): """ Set whether to show the graph's title or not. @type show: boolean. """ self.set_property("show-title", show) self.emit("appearance_changed") def add_data(self, data_list): """ Add data to the graph. data_list should be a list of x,y pairs. If you want to provide error data for a datapoint, the tuple for that point has to be (x, y, xerror, yerror). If you want only one error, set the other to zero. You can mix datapoints with and without error data in data_list. @type data_list: a list (see above). """ new_data, new_errors = separate_data_and_errors(data_list) self._data += new_data self._errors = dict(self._errors, **new_errors) self._range_calc.add_graph(self) def get_data(self): """ Returns the data of the graph. @return: a list of x, y pairs. """ return self._data def set_line_style(self, style): """ Set the line style that should be used for drawing the graph (if type is line_chart.GRAPH_LINES or line_chart.GRAPH_BOTH). style has to be one of these constants: - 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 line style constants above. """ self.set_property("line-style", style) self.emit("appearance_changed") def get_line_style(self): """ Returns the current line style for the graph (see L{set_line_style} for details). @return: a line style constant. """ return self.get_property("line-style") def set_point_style(self, style): """ Set the point style that should be used when drawing the graph (if type is line_chart.GRAPH_POINTS or line_chart.GRAPH_BOTH). For style you can use one of these constants: - pygtk_chart.POINT_STYLE_CIRCLE (default) - pygtk_chart.POINT_STYLE_SQUARE - pygtk_chart.POINT_STYLE_CROSS - pygtk_chart.POINT_STYLE_TRIANGLE_UP - pygtk_chart.POINT_STYLE_TRIANGLE_DOWN - pygtk_chart.POINT_STYLE_DIAMOND style can also be a gtk.gdk.Pixbuf that should be used as point. @param style: the new point style @type style: one of the cosnatnts above or gtk.gdk.Pixbuf. """ self.set_property("point-style", style) self.emit("appearance_changed") def get_point_style(self): """ Returns the current point style. See L{set_point_style} for details. @return: a point style constant or gtk.gdk.Pixbuf. """ return self.get_property("point-style") def set_clickable(self, clickable): """ Set whether the datapoints of the graph should be clickable (only if the datapoints are shown). If this is set to True, the LineChart will emit the signal 'datapoint-clicked' when a datapoint was clicked. @type clickable: boolean. """ self.set_property("clickable", clickable) self.emit("appearance_changed") def get_clickable(self): """ Returns True if the datapoints of the graph are clickable. @return: boolean. """ return self.get_property("clickable") def set_show_xerrors(self, show): """ Use this method to set whether x-errorbars should be shown if error data is available. @type show: boolean. """ self.set_property("show-xerrors", show) self.emit("appearance_changed") def get_show_xerrors(self): """ Returns True if x-errorbars should be drawn if error data is available. @return: boolean. """ return self.get_property("show-xerrors") def set_show_yerrors(self, show): """ Use this method to set whether y-errorbars should be shown if error data is available. @type show: boolean. """ self.set_property("show-yerrors", show) self.emit("appearance_changed") def get_show_yerrors(self): """ Returns True if y-errorbars should be drawn if error data is available. @return: boolean. """ return self.get_property("show-yerrors") def graph_new_from_function(func, xmin, xmax, graph_name, samples=100, do_optimize_sampling=True): """ Returns a line_chart.Graph with data created from the function y = func(x) with x in [xmin, xmax]. The id of the new graph is graph_name. The parameter samples gives the number of points that should be evaluated in [xmin, xmax] (default: 100). If do_optimize_sampling is True (default) additional points will be evaluated to smoothen the curve. @type func: a function @param func: the function to evaluate @type xmin: float @param xmin: the minimum x value to evaluate @type xmax: float @param xmax: the maximum x value to evaluate @type graph_name: string @param graph_name: a unique name for the new graph @type samples: int @param samples: number of samples @type do_optimize_sampling: boolean @param do_optimize_sampling: set whether to add additional points @return: line_chart.Graph """ delta = (xmax - xmin) / float(samples - 1) data = [] x = xmin while x <= xmax: data.append((x, func(x))) x += delta if do_optimize_sampling: data = optimize_sampling(func, data) return Graph(graph_name, "", data) def optimize_sampling(func, data): new_data = [] prev_point = None prev_slope = None for x, y in data: if prev_point != None: if (x - prev_point[0]) == 0: return data slope = (y - prev_point[1]) / (x - prev_point[0]) if prev_slope != None: if abs(slope - prev_slope) >= 0.1: nx = prev_point[0] + (x - prev_point[0]) / 2.0 ny = func(nx) new_data.append((nx, ny)) #print abs(slope - prev_slope), prev_point[0], nx, x prev_slope = slope prev_point = x, y if new_data: data += new_data data.sort(lambda x, y: cmp(x[0], y[0])) return optimize_sampling(func, data) else: return data def graph_new_from_file(filename, graph_name, x_col=0, y_col=1, xerror_col=-1, yerror_col=-1): """ Returns a line_chart.Graph with point taken from data file filename. The id of the new graph is graph_name. Data file format: The columns in the file have to be separated by tabs or one or more spaces. Everything after '#' is ignored (comment). Use the parameters x_col and y_col to control which columns to use for plotting. By default, the first column (x_col=0) is used for x values, the second (y_col=1) is used for y values. The parameters xerror_col and yerror_col should point to the column in which the x/y error values are. If you do not want to provide x or y error data, omit the paramter or set it to -1 (default). @type filename: string @param filename: path to the data file @type graph_name: string @param graph_name: a unique name for the graph @type x_col: int @param x_col: the number of the column to use for x values @type y_col: int @param y_col: the number of the column to use for y values @type xerror_col: int @param xerror_col: index of the column for x error values @type yerror_col: int @param yerror_col: index of the column for y error values @return: line_chart.Graph """ points = [] f = open(filename, "r") data = f.read() f.close() lines = data.split("\n") for line in lines: line = line.strip() #remove special characters at beginning and end #remove comments: a = line.split("#", 1) if a and a[0]: line = a[0] #get data from line: if line.find("\t") != -1: #col separator is tab d = line.split("\t") else: #col separator is one or more space #normalize to one space: while line.find(" ") != -1: line = line.replace(" ", " ") d = line.split(" ") d = filter(lambda x: x, d) d = map(lambda x: float(x), d) new_data = (d[x_col], d[y_col]) if xerror_col != -1 or yerror_col != -1: xerror = 0 yerror = 0 if xerror_col != -1: xerror = d[xerror_col] if yerror_col != -1: yerror = d[yerror_col] new_data = (d[x_col], d[y_col], xerror, yerror) points.append(new_data) return Graph(graph_name, "", points) class Legend(ChartObject): """ This class represents a legend on a line chart. Properties ========== The Legend class inherits properties from chart_object.ChartObject. Additional properties: - position (the legend's position on the chart, type: a corner position constant). Signals ======= The Legend class inherits signals from chart_object.ChartObject. """ __gproperties__ = {"position": (gobject.TYPE_INT, "legend position", "Position of the legend.", 7, 11, 8, gobject.PARAM_READWRITE)} def __init__(self): ChartObject.__init__(self) self._show = False self._position = POSITION_TOP_RIGHT self.last_width = 200 def do_get_property(self, property): if property.name == "visible": return self._show elif property.name == "antialias": return self._antialias elif property.name == "position": return self._position 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 == "position": self._position = value else: raise AttributeError, "Property %s does not exist." % property.name def _do_draw(self, context, rect, graphs): context.set_line_width(1) total_height = rect.height - 1 total_width = 200 label_width = total_width - 12 - 20 x = 0 y = 0 if self._position == POSITION_TOP_RIGHT: x = rect.width - total_width - 16 y = 16 elif self._position == POSITION_BOTTOM_RIGHT: x = rect.width - total_width - 16 y = rect.height - 16 - total_height elif self._position == POSITION_BOTTOM_LEFT: x = 16 y = rect.height - 16 - total_height elif self._position == POSITION_TOP_LEFT: x = 16 y = 16 elif self._position == POSITION_RIGHT: # Bah. This is so broken and horrible and just wont work for small graphs x = rect.width y = 4 # Why 16? context.set_antialias(cairo.ANTIALIAS_NONE) context.set_source_rgb(1, 1, 1) context.rectangle(x, y - 3, total_width, total_height) context.fill_preserve() context.set_source_rgb(0, 0, 0) context.stroke() context.set_antialias(cairo.ANTIALIAS_DEFAULT) for id, graph in graphs.iteritems(): if not graph.get_visible(): continue #draw the label graph_label = label.Label((x + (total_width - label_width), y), graph.get_title(), anchor=label.ANCHOR_TOP_LEFT) graph_label.set_max_width(label_width) graph_label.draw(context, rect) #draw line if graph.get_type() in [GRAPH_LINES, GRAPH_BOTH]: lines = graph_label.get_line_count() line_height = graph_label.get_real_dimensions()[1] / lines set_context_line_style(context, graph.get_line_style()) context.set_source_rgb(*color_gdk_to_cairo(graph.get_color())) context.move_to(x + 6, y + line_height / 2) context.rel_line_to(20, 0) context.stroke() #draw point if graph.get_type() in [GRAPH_POINTS, GRAPH_BOTH]: lines = graph_label.get_line_count() line_height = graph_label.get_real_dimensions()[1] / lines context.set_source_rgb(*color_gdk_to_cairo(graph.get_color())) if type(graph.get_point_style()) != gtk.gdk.Pixbuf: draw_point(context, x + 6 + 20, y + line_height / 2, graph.get_point_size(), graph.get_point_style()) else: draw_point_pixbuf(context, x + 6 + 20, y + line_height / 2, graph.get_point_style()) y += graph_label.get_real_dimensions()[1] + 6 def set_position(self, position): """ Set the position of the legend. position has to be one of these position constants: - line_chart.POSITION_TOP_RIGHT (default) - line_chart.POSITION_BOTTOM_RIGHT - line_chart.POSITION_BOTTOM_LEFT - line_chart.POSITION_TOP_LEFT @param position: the legend's position @type position: one of the constants above. """ self.set_property("position", position) self.emit("appearance_changed") def get_position(self): """ Returns the position of the legend. See L{set_position} for details. @return: a position constant. """ return self.get_property("position")