Merge lp:~zeitgeist/zeitgeist/benchmark-tools into lp:~zeitgeist/zeitgeist/bluebird

Proposed by Seif Lotfy
Status: Merged
Merged at revision: 353
Proposed branch: lp:~zeitgeist/zeitgeist/benchmark-tools
Merge into: lp:~zeitgeist/zeitgeist/bluebird
Diff against target: 4038 lines (+3948/-2)
11 files modified
extensions/Makefile.am (+11/-1)
extensions/benchmark.vala (+127/-0)
src/Makefile.am (+1/-0)
src/extension-collection.vala (+3/-1)
tools/development/README (+23/-0)
tools/development/benchmark.py (+14/-0)
tools/development/cairoplot.py (+2371/-0)
tools/development/query_sets/timerange_always.txt (+9/-0)
tools/development/query_sets/timerange_interval.txt (+9/-0)
tools/development/query_timings.py (+240/-0)
tools/development/series.py (+1140/-0)
To merge this branch: bzr merge lp:~zeitgeist/zeitgeist/benchmark-tools
Reviewer Review Type Date Requested Status
Siegfried Gevatter Approve
Michal Hruby (community) Approve
Review via email: mp+86867@code.launchpad.net

Description of the change

Added Benchmarking tools:
- Extension to plot...
- Plotting tools

To post a comment you must log in.
Revision history for this message
Michal Hruby (mhr3) wrote :

Please remove the json and svg files from version control.

The extension is also missing unload() implementation.

review: Needs Fixing
348. By Seif Lotfy

removed svg and json files

Revision history for this message
Siegfried Gevatter (rainct) wrote :

Lovely :).

Pedantic comments:

 - If you modified query_timings.py, copyright's missing in there.
 - Return extra_data to something else (eg. result), since it's not just "extra data", but all the data :P.
 - Typo: "running and instance of", "argument, multible '--plot' arguments", "zeitgeist" (capitalize), "several types of plots ??such?? using", "find_events, overall)" -> "find_events or overall", "will define multible series." (where does this sentence start? also multiple typo again).
 - tools/development/benchmark.py: Not sure, but I'd replace the numbers with proper constants from zeitgeist.datamodel. You can then also use ZeitgeistClient.get_extension.
 - benchmark.py seems pretty arbitrary.
 - Seriously there isn't a good enough plot library in Python or Debian repository?

349. By Seif Lotfy

added unload method
fixed typos
renamed extra_data to data

Revision history for this message
Michal Hruby (mhr3) wrote :

The only remaining thing I see is how do we want to deal with the extension, obviously it's only meant for development and it probably shouldn't be compiled into Zeitgeist by default (although imo it can as long as it's disabled by default)

review: Needs Information
Revision history for this message
Seif Lotfy (seif) wrote :

I's say not to ship it since it adds some extra dependencies...

On Thu, Dec 29, 2011 at 1:12 AM, Michal Hruby <email address hidden>wrote:

> Review: Needs Information
>
> The only remaining thing I see is how do we want to deal with the
> extension, obviously it's only meant for development and it probably
> shouldn't be compiled into Zeitgeist by default (although imo it can as
> long as it's disabled by default)
> --
>
> https://code.launchpad.net/~zeitgeist/zeitgeist/benchmark-tools/+merge/86867
> You proposed lp:~zeitgeist/zeitgeist/benchmark-tools for merging.
>

Revision history for this message
Michal Hruby (mhr3) wrote :

As discussed on IRC we'll ship the extension by default but it'll be disabled, the only problem is that disabling extensions is not yet supported, so we'll fix that soon.

review: Approve
Revision history for this message
Siegfried Gevatter (rainct) wrote :

OK, but I don't quite like the cairoplot.py copy in there.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'extensions/Makefile.am'
2--- extensions/Makefile.am 2011-10-31 15:28:09 +0000
3+++ extensions/Makefile.am 2011-12-28 19:50:03 +0000
4@@ -3,7 +3,7 @@
5 NULL =
6
7 #extensionsdir = $(libdir)/zeitgeist/extensions
8-noinst_LTLIBRARIES = ds-registry.la blacklist.la storage-monitor.la fts.la
9+noinst_LTLIBRARIES = ds-registry.la blacklist.la storage-monitor.la fts.la benchmark.la
10
11 AM_CPPFLAGS = \
12 $(ZEITGEIST_CFLAGS) \
13@@ -60,3 +60,13 @@
14 fts_la_LIBADD = \
15 $(ZEITGEIST_LIBS) \
16 $(NULL)
17+
18+benchmark_la_SOURCES = \
19+ benchmark.vala \
20+ $(NULL)
21+
22+benchmark_la_LDFLAGS = -module -avoid-version
23+
24+benchmark_la_LIBADD = \
25+ $(ZEITGEIST_LIBS) \
26+ $(NULL)
27
28=== added file 'extensions/benchmark.vala'
29--- extensions/benchmark.vala 1970-01-01 00:00:00 +0000
30+++ extensions/benchmark.vala 2011-12-28 19:50:03 +0000
31@@ -0,0 +1,127 @@
32+/* fts.vala
33+ *
34+ * Copyright © 2011 Collabora Ltd.
35+ * By Seif Lotfy <seif@lotfy.com>
36+ *
37+ * This program is free software: you can redistribute it and/or modify
38+ * it under the terms of the GNU Lesser General Public License as published by
39+ * the Free Software Foundation, either version 2.1 of the License, or
40+ * (at your option) any later version.
41+ *
42+ * This program is distributed in the hope that it will be useful,
43+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
44+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
45+ * GNU General Public License for more details.
46+ *
47+ * You should have received a copy of the GNU Lesser General Public License
48+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
49+ *
50+ */
51+
52+namespace Zeitgeist
53+{
54+ [DBus (name = "org.gnome.zeitgeist.Benchmark")]
55+ public interface RemoteBenchmarker: Object
56+ {
57+ public abstract async HashTable<string, Variant> find_events (
58+ [DBus (signature = "(xx)")] Variant time_range,
59+ [DBus (signature = "a(asaasay)")] Variant event_templates,
60+ uint storage_state, uint num_events, uint result_type)
61+ throws Error;
62+ }
63+
64+ class Benchmarker: Extension, RemoteBenchmarker
65+ {
66+
67+ private uint registration_id;
68+
69+ Benchmarker ()
70+ {
71+ Object ();
72+ }
73+
74+ construct
75+ {
76+ try
77+ {
78+ var connection = Bus.get_sync (BusType.SESSION, null);
79+ registration_id = connection.register_object<RemoteBenchmarker> (
80+ "/org/gnome/zeitgeist/benchmark", this);
81+ }
82+ catch (Error err)
83+ {
84+ warning ("%s", err.message);
85+ }
86+ }
87+
88+ public async HashTable<string, Variant> find_events (Variant time_range,
89+ Variant filter_templates, uint storage_state, uint num_events,
90+ uint result_type)
91+ throws Error
92+ {
93+ var data = new HashTable<string, Variant> (str_hash, str_equal);
94+
95+ var find_event_ids_timer = new Timer ();
96+ var ids = engine.find_event_ids (
97+ new TimeRange.from_variant (time_range),
98+ Events.from_variant (filter_templates),
99+ storage_state, num_events, result_type);
100+ var find_event_ids_elapsed = find_event_ids_timer.elapsed();
101+
102+ var get_events_timer = new Timer ();
103+ var events = engine.get_events (ids);
104+ var get_events_elapsed = get_events_timer.elapsed();
105+
106+ var marsh_events_timer = new Timer ();
107+ var marsh_events = Events.to_variant(events);
108+ var marsh_events_elapsed = marsh_events_timer.elapsed();
109+
110+ var find_events_elapsed = get_events_elapsed + find_event_ids_elapsed + marsh_events_elapsed;
111+
112+ data.insert("find_event_ids",
113+ new Variant.double(find_event_ids_elapsed));
114+ data.insert("get_events",
115+ new Variant.double(get_events_elapsed));
116+ data.insert("find_events",
117+ new Variant.double(find_events_elapsed));
118+ data.insert("marsh_events",
119+ new Variant.double(marsh_events_elapsed));
120+ data.insert("events", marsh_events);
121+
122+ return data;
123+ }
124+
125+ public override void unload ()
126+ {
127+ try
128+ {
129+ var connection = Bus.get_sync (BusType.SESSION, null);
130+ if (registration_id != 0)
131+ {
132+ connection.unregister_object (registration_id);
133+ registration_id = 0;
134+ }
135+ }
136+ catch (Error err)
137+ {
138+ warning ("%s", err.message);
139+ }
140+
141+ debug ("%s, this.ref_count = %u", Log.METHOD, this.ref_count);
142+ }
143+
144+ }
145+
146+ [ModuleInit]
147+#if BUILTIN_EXTENSIONS
148+ public static Type benchmark_init (TypeModule module)
149+ {
150+#else
151+ public static Type extension_register (TypeModule module)
152+ {
153+#endif
154+ return typeof (Benchmarker);
155+ }
156+}
157+
158+// vim:expandtab:ts=4:sw=4
159
160=== modified file 'src/Makefile.am'
161--- src/Makefile.am 2011-11-01 18:30:37 +0000
162+++ src/Makefile.am 2011-12-28 19:50:03 +0000
163@@ -25,6 +25,7 @@
164 ext-histogram.vala \
165 ext-storage-monitor.vala \
166 ext-fts.vala \
167+ ext-benchmark.vala \
168 $(NULL)
169
170 zeitgeist_daemon_VALASOURCES = \
171
172=== added symlink 'src/ext-benchmark.vala'
173=== target is u'/home/seif/Projects/zeitgeist/extensions/benchmark.vala'
174=== modified file 'src/extension-collection.vala'
175--- src/extension-collection.vala 2011-10-20 11:20:36 +0000
176+++ src/extension-collection.vala 2011-12-28 19:50:03 +0000
177@@ -48,7 +48,8 @@
178 blacklist_init,
179 histogram_init,
180 storage_monitor_init,
181- fts_init
182+ fts_init,
183+ benchmark_init
184 };
185
186 foreach (var func in builtins)
187@@ -163,6 +164,7 @@
188 private extern static Type histogram_init (TypeModule mod);
189 private extern static Type storage_monitor_init (TypeModule mod);
190 private extern static Type fts_init (TypeModule mod);
191+ private extern static Type benchmark_init (TypeModule mod);
192 #endif
193
194 }
195
196=== added directory 'tools'
197=== added directory 'tools/development'
198=== added file 'tools/development/README'
199--- tools/development/README 1970-01-01 00:00:00 +0000
200+++ tools/development/README 2011-12-28 19:50:03 +0000
201@@ -0,0 +1,23 @@
202+# USAGE
203+#
204+# To run the benchmarks on a certain branch of Zeitgeist make sure you are
205+# running an instance of the branch. Make sure the branch includes the benchmark
206+# extension.
207+#
208+# To benchmark and output timing to 'output.json' run
209+# ./query_timings.py -o output.json
210+# If you already have data in the output file and want to merge both
211+# data sets (build avg.) run
212+# ./query_timings.py -m -o output.json
213+# To plot the data use the '--plot' argument, multiple '--plot' arguments
214+# will define multiple series.
215+# When plotting one needs to choose between several types of plots using
216+# --type with arguments marsh_time, get_events_time, find_ids_time, find_events and overall
217+#
218+# In short, a run always looks like:
219+# tools/development/query_timings.py --name "lp:zeitgeist" -o trunk.json \
220+# --queries tools/development/query_sets/timerange_always.txt
221+# tools/development/query_timings.py --name "lp:some-branch" -o somebranch.json \
222+# --queries tools/development/query_sets/timerange_always.txt
223+# tools/development/query_timings.py --plot --type marsh_time somebranch.json --plot trunk.json -o benchmark.svg
224+
225
226=== added file 'tools/development/benchmark.py'
227--- tools/development/benchmark.py 1970-01-01 00:00:00 +0000
228+++ tools/development/benchmark.py 2011-12-28 19:50:03 +0000
229@@ -0,0 +1,14 @@
230+import dbus
231+
232+BUS_NAME = "org.gnome.zeitgeist.Engine"
233+INTERFACE_NAME = "org.gnome.zeitgeist.Benchmark"
234+OBJECT_PATH = "/org/gnome/zeitgeist/benchmark"
235+
236+bus = dbus.SessionBus()
237+benchmark_obj = bus.get_object(BUS_NAME, OBJECT_PATH)
238+benchmark_interface = dbus.Interface(benchmark_obj,
239+ dbus_interface = INTERFACE_NAME)
240+
241+def find_events(time_frame, templates, storage_type, num_events, result_type):
242+ return benchmark_interface.FindEvents(time_frame, templates, storage_type,
243+ num_events, result_type)
244
245=== added file 'tools/development/cairoplot.py'
246--- tools/development/cairoplot.py 1970-01-01 00:00:00 +0000
247+++ tools/development/cairoplot.py 2011-12-28 19:50:03 +0000
248@@ -0,0 +1,2371 @@
249+#!/usr/bin/env python
250+# -*- coding: utf-8 -*-
251+
252+# CairoPlot.py
253+#
254+# Copyright (c) 2008 Rodrigo Moreira Araújo
255+#
256+# Author: Rodrigo Moreiro Araujo <alf.rodrigo@gmail.com>
257+# Markus Korn <thekorn@gmx.de>
258+#
259+# This program is free software; you can redistribute it and/or
260+# modify it under the terms of the GNU Lesser General Public License
261+# as published by the Free Software Foundation; either version 2 of
262+# the License, or (at your option) any later version.
263+#
264+# This program is distributed in the hope that it will be useful,
265+# but WITHOUT ANY WARRANTY; without even the implied warranty of
266+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
267+# GNU General Public License for more details.
268+#
269+# You should have received a copy of the GNU Lesser General Public
270+# License along with this program; if not, write to the Free Software
271+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
272+# USA
273+
274+#Contributor: João S. O. Bueno
275+
276+#TODO: review BarPlot Code
277+#TODO: x_label colision problem on Horizontal Bar Plot
278+#TODO: y_label's eat too much space on HBP
279+
280+# thekorn: added optional borders around vertical bars in VerticalBarPlot
281+# thekorn: break long x-axis labels into two lines
282+
283+__version__ = 1.1
284+
285+import cairo
286+import math
287+import random
288+from series import Series, Group, Data
289+
290+HORZ = 0
291+VERT = 1
292+NORM = 2
293+
294+COLORS = {"red" : (1.0,0.0,0.0,1.0), "lime" : (0.0,1.0,0.0,1.0), "blue" : (0.0,0.0,1.0,1.0),
295+ "maroon" : (0.5,0.0,0.0,1.0), "green" : (0.0,0.5,0.0,1.0), "navy" : (0.0,0.0,0.5,1.0),
296+ "yellow" : (1.0,1.0,0.0,1.0), "magenta" : (1.0,0.0,1.0,1.0), "cyan" : (0.0,1.0,1.0,1.0),
297+ "orange" : (1.0,0.5,0.0,1.0), "white" : (1.0,1.0,1.0,1.0), "black" : (0.0,0.0,0.0,1.0),
298+ "gray" : (0.5,0.5,0.5,1.0), "light_gray" : (0.9,0.9,0.9,1.0),
299+ "transparent" : (0.0,0.0,0.0,0.0)}
300+
301+THEMES = {"black_red" : [(0.0,0.0,0.0,1.0), (1.0,0.0,0.0,1.0)],
302+ "red_green_blue" : [(1.0,0.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0)],
303+ "red_orange_yellow" : [(1.0,0.2,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,1.0,0.0,1.0)],
304+ "yellow_orange_red" : [(1.0,1.0,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,0.2,0.0,1.0)],
305+ "rainbow" : [(1.0,0.0,0.0,1.0), (1.0,0.5,0.0,1.0), (1.0,1.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0), (0.3, 0.0, 0.5,1.0), (0.5, 0.0, 1.0, 1.0)]}
306+
307+def colors_from_theme( theme, series_length, mode = 'solid' ):
308+ colors = []
309+ if theme not in THEMES.keys() :
310+ raise Exception, "Theme not defined"
311+ color_steps = THEMES[theme]
312+ n_colors = len(color_steps)
313+ if series_length <= n_colors:
314+ colors = [color + tuple([mode]) for color in color_steps[0:n_colors]]
315+ else:
316+ iterations = [(series_length - n_colors)/(n_colors - 1) for i in color_steps[:-1]]
317+ over_iterations = (series_length - n_colors) % (n_colors - 1)
318+ for i in range(n_colors - 1):
319+ if over_iterations <= 0:
320+ break
321+ iterations[i] += 1
322+ over_iterations -= 1
323+ for index,color in enumerate(color_steps[:-1]):
324+ colors.append(color + tuple([mode]))
325+ if iterations[index] == 0:
326+ continue
327+ next_color = color_steps[index+1]
328+ color_step = ((next_color[0] - color[0])/(iterations[index] + 1),
329+ (next_color[1] - color[1])/(iterations[index] + 1),
330+ (next_color[2] - color[2])/(iterations[index] + 1),
331+ (next_color[3] - color[3])/(iterations[index] + 1))
332+ for i in range( iterations[index] ):
333+ colors.append((color[0] + color_step[0]*(i+1),
334+ color[1] + color_step[1]*(i+1),
335+ color[2] + color_step[2]*(i+1),
336+ color[3] + color_step[3]*(i+1),
337+ mode))
338+ colors.append(color_steps[-1] + tuple([mode]))
339+ return colors
340+
341+
342+def other_direction(direction):
343+ "explicit is better than implicit"
344+ if direction == HORZ:
345+ return VERT
346+ else:
347+ return HORZ
348+
349+#Class definition
350+
351+class Plot(object):
352+ def __init__(self,
353+ surface=None,
354+ data=None,
355+ width=640,
356+ height=480,
357+ background=None,
358+ border = 0,
359+ x_labels = None,
360+ y_labels = None,
361+ series_colors = None):
362+ random.seed(2)
363+ self.create_surface(surface, width, height)
364+ self.dimensions = {}
365+ self.dimensions[HORZ] = width
366+ self.dimensions[VERT] = height
367+ self.context = cairo.Context(self.surface)
368+ self.labels={}
369+ self.labels[HORZ] = x_labels
370+ self.labels[VERT] = y_labels
371+ self.load_series(data, x_labels, y_labels, series_colors)
372+ self.font_size = 10
373+ self.set_background (background)
374+ self.border = border
375+ self.borders = {}
376+ self.line_color = (0.5, 0.5, 0.5)
377+ self.line_width = 0.5
378+ self.label_color = (0.0, 0.0, 0.0)
379+ self.grid_color = (0.8, 0.8, 0.8)
380+
381+ def create_surface(self, surface, width=None, height=None):
382+ self.filename = None
383+ if isinstance(surface, cairo.Surface):
384+ self.surface = surface
385+ return
386+ if not type(surface) in (str, unicode):
387+ raise TypeError("Surface should be either a Cairo surface or a filename, not %s" % surface)
388+ sufix = surface.rsplit(".")[-1].lower()
389+ self.filename = surface
390+ if sufix == "png":
391+ self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
392+ elif sufix == "ps":
393+ self.surface = cairo.PSSurface(surface, width, height)
394+ elif sufix == "pdf":
395+ self.surface = cairo.PSSurface(surface, width, height)
396+ else:
397+ if sufix != "svg":
398+ self.filename += ".svg"
399+ self.surface = cairo.SVGSurface(self.filename, width, height)
400+
401+ def commit(self):
402+ try:
403+ self.context.show_page()
404+ if self.filename and self.filename.endswith(".png"):
405+ self.surface.write_to_png(self.filename)
406+ else:
407+ self.surface.finish()
408+ except cairo.Error:
409+ pass
410+
411+ def load_series (self, data, x_labels=None, y_labels=None, series_colors=None):
412+ self.series_labels = []
413+ self.series = None
414+
415+ #The pretty way
416+ #if not isinstance(data, Series):
417+ # # Not an instance of Series
418+ # self.series = Series(data)
419+ #else:
420+ # self.series = data
421+ #
422+ #self.series_labels = self.series.get_names()
423+
424+ #TODO: Remove on next version
425+ # The ugly way, keeping retrocompatibility...
426+ if callable(data) or type(data) is list and callable(data[0]): # Lambda or List of lambdas
427+ self.series = data
428+ self.series_labels = None
429+ elif isinstance(data, Series): # Instance of Series
430+ self.series = data
431+ self.series_labels = data.get_names()
432+ else: # Anything else
433+ self.series = Series(data)
434+ self.series_labels = self.series.get_names()
435+
436+ #TODO: allow user passed series_widths
437+ self.series_widths = [1.0 for group in self.series]
438+
439+ #TODO: Remove on next version
440+ self.process_colors( series_colors )
441+
442+ def process_colors( self, series_colors, length = None, mode = 'solid' ):
443+ #series_colors might be None, a theme, a string of colors names or a list of color tuples
444+ if length is None :
445+ length = len( self.series.to_list() )
446+
447+ #no colors passed
448+ if not series_colors:
449+ #Randomize colors
450+ self.series_colors = [ [random.random() for i in range(3)] + [1.0, mode] for series in range( length ) ]
451+ else:
452+ #Just theme pattern
453+ if not hasattr( series_colors, "__iter__" ):
454+ theme = series_colors
455+ self.series_colors = colors_from_theme( theme.lower(), length )
456+
457+ #Theme pattern and mode
458+ elif not hasattr(series_colors, '__delitem__') and not hasattr( series_colors[0], "__iter__" ):
459+ theme = series_colors[0]
460+ mode = series_colors[1]
461+ self.series_colors = colors_from_theme( theme.lower(), length, mode )
462+
463+ #List
464+ else:
465+ self.series_colors = series_colors
466+ for index, color in enumerate( self.series_colors ):
467+ #element is a color name
468+ if not hasattr(color, "__iter__"):
469+ self.series_colors[index] = COLORS[color.lower()] + tuple([mode])
470+ #element is rgb tuple instead of rgba
471+ elif len( color ) == 3 :
472+ self.series_colors[index] += (1.0,mode)
473+ #element has 4 elements, might be rgba tuple or rgb tuple with mode
474+ elif len( color ) == 4 :
475+ #last element is mode
476+ if not hasattr(color[3], "__iter__"):
477+ self.series_colors[index] += tuple([color[3]])
478+ self.series_colors[index][3] = 1.0
479+ #last element is alpha
480+ else:
481+ self.series_colors[index] += tuple([mode])
482+
483+ def get_width(self):
484+ return self.surface.get_width()
485+
486+ def get_height(self):
487+ return self.surface.get_height()
488+
489+ def set_background(self, background):
490+ if background is None:
491+ self.background = (0.0,0.0,0.0,0.0)
492+ elif type(background) in (cairo.LinearGradient, tuple):
493+ self.background = background
494+ elif not hasattr(background,"__iter__"):
495+ colors = background.split(" ")
496+ if len(colors) == 1 and colors[0] in COLORS:
497+ self.background = COLORS[background]
498+ elif len(colors) > 1:
499+ self.background = cairo.LinearGradient(self.dimensions[HORZ] / 2, 0, self.dimensions[HORZ] / 2, self.dimensions[VERT])
500+ for index,color in enumerate(colors):
501+ self.background.add_color_stop_rgba(float(index)/(len(colors)-1),*COLORS[color])
502+ else:
503+ raise TypeError ("Background should be either cairo.LinearGradient or a 3/4-tuple, not %s" % type(background))
504+
505+ def render_background(self):
506+ if isinstance(self.background, cairo.LinearGradient):
507+ self.context.set_source(self.background)
508+ else:
509+ self.context.set_source_rgba(*self.background)
510+ self.context.rectangle(0,0, self.dimensions[HORZ], self.dimensions[VERT])
511+ self.context.fill()
512+
513+ def render_bounding_box(self):
514+ self.context.set_source_rgba(*self.line_color)
515+ self.context.set_line_width(self.line_width)
516+ self.context.rectangle(self.border, self.border,
517+ self.dimensions[HORZ] - 2 * self.border,
518+ self.dimensions[VERT] - 2 * self.border)
519+ self.context.stroke()
520+
521+ def render(self):
522+ pass
523+
524+class ScatterPlot( Plot ):
525+ def __init__(self,
526+ surface=None,
527+ data=None,
528+ errorx=None,
529+ errory=None,
530+ width=640,
531+ height=480,
532+ background=None,
533+ border=0,
534+ axis = False,
535+ dash = False,
536+ discrete = False,
537+ dots = 0,
538+ grid = False,
539+ series_legend = False,
540+ x_labels = None,
541+ y_labels = None,
542+ x_bounds = None,
543+ y_bounds = None,
544+ z_bounds = None,
545+ x_title = None,
546+ y_title = None,
547+ series_colors = None,
548+ circle_colors = None ):
549+
550+ self.bounds = {}
551+ self.bounds[HORZ] = x_bounds
552+ self.bounds[VERT] = y_bounds
553+ self.bounds[NORM] = z_bounds
554+ self.titles = {}
555+ self.titles[HORZ] = x_title
556+ self.titles[VERT] = y_title
557+ self.max_value = {}
558+ self.axis = axis
559+ self.discrete = discrete
560+ self.dots = dots
561+ self.grid = grid
562+ self.series_legend = series_legend
563+ self.variable_radius = False
564+ self.x_label_angle = math.pi / 2.5
565+ self.circle_colors = circle_colors
566+
567+ Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors)
568+
569+ self.dash = None
570+ if dash:
571+ if hasattr(dash, "keys"):
572+ self.dash = [dash[key] for key in self.series_labels]
573+ elif max([hasattr(item,'__delitem__') for item in data]) :
574+ self.dash = dash
575+ else:
576+ self.dash = [dash]
577+
578+ self.load_errors(errorx, errory)
579+
580+ def convert_list_to_tuple(self, data):
581+ #Data must be converted from lists of coordinates to a single
582+ # list of tuples
583+ out_data = zip(*data)
584+ if len(data) == 3:
585+ self.variable_radius = True
586+ return out_data
587+
588+ def load_series(self, data, x_labels = None, y_labels = None, series_colors=None):
589+ #TODO: In cairoplot 2.0 keep only the Series instances
590+
591+ # Convert Data and Group to Series
592+ if isinstance(data, Data) or isinstance(data, Group):
593+ data = Series(data)
594+
595+ # Series
596+ if isinstance(data, Series):
597+ for group in data:
598+ for item in group:
599+ if len(item) is 3:
600+ self.variable_radius = True
601+
602+ #Dictionary with lists
603+ if hasattr(data, "keys") :
604+ if hasattr( data.values()[0][0], "__delitem__" ) :
605+ for key in data.keys() :
606+ data[key] = self.convert_list_to_tuple(data[key])
607+ elif len(data.values()[0][0]) == 3:
608+ self.variable_radius = True
609+ #List
610+ elif hasattr(data[0], "__delitem__") :
611+ #List of lists
612+ if hasattr(data[0][0], "__delitem__") :
613+ for index,value in enumerate(data) :
614+ data[index] = self.convert_list_to_tuple(value)
615+ #List
616+ elif type(data[0][0]) != type((0,0)):
617+ data = self.convert_list_to_tuple(data)
618+ #Three dimensional data
619+ elif len(data[0][0]) == 3:
620+ self.variable_radius = True
621+
622+ #List with three dimensional tuples
623+ elif len(data[0]) == 3:
624+ self.variable_radius = True
625+ Plot.load_series(self, data, x_labels, y_labels, series_colors)
626+ self.calc_boundaries()
627+ self.calc_labels()
628+
629+ def load_errors(self, errorx, errory):
630+ self.errors = None
631+ if errorx == None and errory == None:
632+ return
633+ self.errors = {}
634+ self.errors[HORZ] = None
635+ self.errors[VERT] = None
636+ #asimetric errors
637+ if errorx and hasattr(errorx[0], "__delitem__"):
638+ self.errors[HORZ] = errorx
639+ #simetric errors
640+ elif errorx:
641+ self.errors[HORZ] = [errorx]
642+ #asimetric errors
643+ if errory and hasattr(errory[0], "__delitem__"):
644+ self.errors[VERT] = errory
645+ #simetric errors
646+ elif errory:
647+ self.errors[VERT] = [errory]
648+
649+ def calc_labels(self):
650+ if not self.labels[HORZ]:
651+ amplitude = self.bounds[HORZ][1] - self.bounds[HORZ][0]
652+ if amplitude % 10: #if horizontal labels need floating points
653+ self.labels[HORZ] = ["%.2lf" % (float(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ]
654+ else:
655+ self.labels[HORZ] = ["%d" % (int(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ]
656+ if not self.labels[VERT]:
657+ amplitude = self.bounds[VERT][1] - self.bounds[VERT][0]
658+ if amplitude % 10: #if vertical labels need floating points
659+ self.labels[VERT] = ["%.2lf" % (float(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ]
660+ else:
661+ self.labels[VERT] = ["%d" % (int(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ]
662+
663+ def calc_extents(self, direction):
664+ self.context.set_font_size(self.font_size * 0.8)
665+ self.max_value[direction] = max(self.context.text_extents(item)[2] for item in self.labels[direction])
666+ self.borders[other_direction(direction)] = self.max_value[direction] + self.border + 20
667+
668+ def calc_boundaries(self):
669+ #HORZ = 0, VERT = 1, NORM = 2
670+ min_data_value = [0,0,0]
671+ max_data_value = [0,0,0]
672+
673+ for group in self.series:
674+ if type(group[0].content) in (int, float, long):
675+ group = [Data((index, item.content)) for index,item in enumerate(group)]
676+
677+ for point in group:
678+ for index, item in enumerate(point.content):
679+ if item > max_data_value[index]:
680+ max_data_value[index] = item
681+ elif item < min_data_value[index]:
682+ min_data_value[index] = item
683+
684+ if not self.bounds[HORZ]:
685+ self.bounds[HORZ] = (min_data_value[HORZ], max_data_value[HORZ])
686+ if not self.bounds[VERT]:
687+ self.bounds[VERT] = (min_data_value[VERT], max_data_value[VERT])
688+ if not self.bounds[NORM]:
689+ self.bounds[NORM] = (min_data_value[NORM], max_data_value[NORM])
690+
691+ def calc_all_extents(self):
692+ self.calc_extents(HORZ)
693+ self.calc_extents(VERT)
694+
695+ self.plot_height = self.dimensions[VERT] - 2 * self.borders[VERT]
696+ self.plot_width = self.dimensions[HORZ] - 2* self.borders[HORZ]
697+
698+ self.plot_top = self.dimensions[VERT] - self.borders[VERT]
699+
700+ def calc_steps(self):
701+ #Calculates all the x, y, z and color steps
702+ series_amplitude = [self.bounds[index][1] - self.bounds[index][0] for index in range(3)]
703+
704+ if series_amplitude[HORZ]:
705+ self.horizontal_step = float (self.plot_width) / series_amplitude[HORZ]
706+ else:
707+ self.horizontal_step = 0.00
708+
709+ if series_amplitude[VERT]:
710+ self.vertical_step = float (self.plot_height) / series_amplitude[VERT]
711+ else:
712+ self.vertical_step = 0.00
713+
714+ if series_amplitude[NORM]:
715+ if self.variable_radius:
716+ self.z_step = float (self.bounds[NORM][1]) / series_amplitude[NORM]
717+ if self.circle_colors:
718+ self.circle_color_step = tuple([float(self.circle_colors[1][i]-self.circle_colors[0][i])/series_amplitude[NORM] for i in range(4)])
719+ else:
720+ self.z_step = 0.00
721+ self.circle_color_step = ( 0.0, 0.0, 0.0, 0.0 )
722+
723+ def get_circle_color(self, value):
724+ return tuple( [self.circle_colors[0][i] + value*self.circle_color_step[i] for i in range(4)] )
725+
726+ def render(self):
727+ self.calc_all_extents()
728+ self.calc_steps()
729+ self.render_background()
730+ self.render_bounding_box()
731+ if self.axis:
732+ self.render_axis()
733+ if self.grid:
734+ self.render_grid()
735+ self.render_labels()
736+ self.render_plot()
737+ if self.errors:
738+ self.render_errors()
739+ if self.series_legend and self.series_labels:
740+ self.render_legend()
741+
742+ def render_axis(self):
743+ #Draws both the axis lines and their titles
744+ cr = self.context
745+ cr.set_source_rgba(*self.line_color)
746+ cr.move_to(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT])
747+ cr.line_to(self.borders[HORZ], self.borders[VERT])
748+ cr.stroke()
749+
750+ cr.move_to(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT])
751+ cr.line_to(self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT])
752+ cr.stroke()
753+
754+ cr.set_source_rgba(*self.label_color)
755+ self.context.set_font_size( 1.2 * self.font_size )
756+ if self.titles[HORZ]:
757+ title_width,title_height = cr.text_extents(self.titles[HORZ])[2:4]
758+ cr.move_to( self.dimensions[HORZ]/2 - title_width/2, self.borders[VERT] - title_height/2 )
759+ cr.show_text( self.titles[HORZ] )
760+
761+ if self.titles[VERT]:
762+ title_width,title_height = cr.text_extents(self.titles[VERT])[2:4]
763+ cr.move_to( self.dimensions[HORZ] - self.borders[HORZ] + title_height/2, self.dimensions[VERT]/2 - title_width/2)
764+ cr.save()
765+ cr.rotate( math.pi/2 )
766+ cr.show_text( self.titles[VERT] )
767+ cr.restore()
768+
769+ def render_grid(self):
770+ cr = self.context
771+ horizontal_step = float( self.plot_height ) / ( len( self.labels[VERT] ) - 1 )
772+ vertical_step = float( self.plot_width ) / ( len( self.labels[HORZ] ) - 1 )
773+
774+ x = self.borders[HORZ] + vertical_step
775+ y = self.plot_top - horizontal_step
776+
777+ for label in self.labels[HORZ][:-1]:
778+ cr.set_source_rgba(*self.grid_color)
779+ cr.move_to(x, self.dimensions[VERT] - self.borders[VERT])
780+ cr.line_to(x, self.borders[VERT])
781+ cr.stroke()
782+ x += vertical_step
783+ for label in self.labels[VERT][:-1]:
784+ cr.set_source_rgba(*self.grid_color)
785+ cr.move_to(self.borders[HORZ], y)
786+ cr.line_to(self.dimensions[HORZ] - self.borders[HORZ], y)
787+ cr.stroke()
788+ y -= horizontal_step
789+
790+ def render_labels(self):
791+ self.context.set_font_size(self.font_size * 0.8)
792+ self.render_horz_labels()
793+ self.render_vert_labels()
794+
795+ def render_horz_labels(self):
796+ cr = self.context
797+ step = float( self.plot_width ) / ( len( self.labels[HORZ] ) - 1 )
798+ x = self.borders[HORZ]
799+ y = self.dimensions[VERT] - self.borders[VERT] + 5
800+
801+ # store rotation matrix from the initial state
802+ rotation_matrix = cr.get_matrix()
803+ rotation_matrix.rotate(self.x_label_angle)
804+
805+ cr.set_source_rgba(*self.label_color)
806+
807+ for item in self.labels[HORZ]:
808+ width = cr.text_extents(item)[2]
809+ cr.move_to(x, y)
810+ cr.save()
811+ cr.set_matrix(rotation_matrix)
812+ cr.show_text(item)
813+ cr.restore()
814+ x += step
815+
816+ def render_vert_labels(self):
817+ cr = self.context
818+ step = ( self.plot_height ) / ( len( self.labels[VERT] ) - 1 )
819+ y = self.plot_top
820+ cr.set_source_rgba(*self.label_color)
821+ for item in self.labels[VERT]:
822+ width = cr.text_extents(item)[2]
823+ cr.move_to(self.borders[HORZ] - width - 5,y)
824+ cr.show_text(item)
825+ y -= step
826+
827+ def render_legend(self):
828+ cr = self.context
829+ cr.set_font_size(self.font_size)
830+ cr.set_line_width(self.line_width)
831+
832+ widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2])
833+ tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3])
834+ max_width = self.context.text_extents(widest_word)[2]
835+ max_height = self.context.text_extents(tallest_word)[3] * 1.1
836+
837+ color_box_height = max_height / 2
838+ color_box_width = color_box_height * 2
839+
840+ #Draw a bounding box
841+ bounding_box_width = max_width + color_box_width + 15
842+ bounding_box_height = (len(self.series_labels)+0.5) * max_height
843+ cr.set_source_rgba(1,1,1)
844+ cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - bounding_box_width, self.borders[VERT],
845+ bounding_box_width, bounding_box_height)
846+ cr.fill()
847+
848+ cr.set_source_rgba(*self.line_color)
849+ cr.set_line_width(self.line_width)
850+ cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - bounding_box_width, self.borders[VERT],
851+ bounding_box_width, bounding_box_height)
852+ cr.stroke()
853+
854+ for idx,key in enumerate(self.series_labels):
855+ #Draw color box
856+ cr.set_source_rgba(*self.series_colors[idx][:4])
857+ cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - max_width - color_box_width - 10,
858+ self.borders[VERT] + color_box_height + (idx*max_height) ,
859+ color_box_width, color_box_height)
860+ cr.fill()
861+
862+ cr.set_source_rgba(0, 0, 0)
863+ cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - max_width - color_box_width - 10,
864+ self.borders[VERT] + color_box_height + (idx*max_height),
865+ color_box_width, color_box_height)
866+ cr.stroke()
867+
868+ #Draw series labels
869+ cr.set_source_rgba(0, 0, 0)
870+ cr.move_to(self.dimensions[HORZ] - self.borders[HORZ] - max_width - 5, self.borders[VERT] + ((idx+1)*max_height))
871+ cr.show_text(key)
872+
873+ def render_errors(self):
874+ cr = self.context
875+ cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height)
876+ cr.clip()
877+ radius = self.dots
878+ x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step
879+ y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step
880+ for index, group in enumerate(self.series):
881+ cr.set_source_rgba(*self.series_colors[index][:4])
882+ for number, data in enumerate(group):
883+ x = x0 + self.horizontal_step * data.content[0]
884+ y = self.dimensions[VERT] - y0 - self.vertical_step * data.content[1]
885+ if self.errors[HORZ]:
886+ cr.move_to(x, y)
887+ x1 = x - self.horizontal_step * self.errors[HORZ][0][number]
888+ cr.line_to(x1, y)
889+ cr.line_to(x1, y - radius)
890+ cr.line_to(x1, y + radius)
891+ cr.stroke()
892+ if self.errors[HORZ] and len(self.errors[HORZ]) == 2:
893+ cr.move_to(x, y)
894+ x1 = x + self.horizontal_step * self.errors[HORZ][1][number]
895+ cr.line_to(x1, y)
896+ cr.line_to(x1, y - radius)
897+ cr.line_to(x1, y + radius)
898+ cr.stroke()
899+ if self.errors[VERT]:
900+ cr.move_to(x, y)
901+ y1 = y + self.vertical_step * self.errors[VERT][0][number]
902+ cr.line_to(x, y1)
903+ cr.line_to(x - radius, y1)
904+ cr.line_to(x + radius, y1)
905+ cr.stroke()
906+ if self.errors[VERT] and len(self.errors[VERT]) == 2:
907+ cr.move_to(x, y)
908+ y1 = y - self.vertical_step * self.errors[VERT][1][number]
909+ cr.line_to(x, y1)
910+ cr.line_to(x - radius, y1)
911+ cr.line_to(x + radius, y1)
912+ cr.stroke()
913+
914+
915+ def render_plot(self):
916+ cr = self.context
917+ if self.discrete:
918+ cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height)
919+ cr.clip()
920+ x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step
921+ y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step
922+ radius = self.dots
923+ for number, group in enumerate (self.series):
924+ cr.set_source_rgba(*self.series_colors[number][:4])
925+ for data in group :
926+ if self.variable_radius:
927+ radius = data.content[2]*self.z_step
928+ if self.circle_colors:
929+ cr.set_source_rgba( *self.get_circle_color( data.content[2]) )
930+ x = x0 + self.horizontal_step*data.content[0]
931+ y = y0 + self.vertical_step*data.content[1]
932+ cr.arc(x, self.dimensions[VERT] - y, radius, 0, 2*math.pi)
933+ cr.fill()
934+ else:
935+ cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height)
936+ cr.clip()
937+ x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step
938+ y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step
939+ radius = self.dots
940+ for number, group in enumerate (self.series):
941+ last_data = None
942+ cr.set_source_rgba(*self.series_colors[number][:4])
943+ for data in group :
944+ x = x0 + self.horizontal_step*data.content[0]
945+ y = y0 + self.vertical_step*data.content[1]
946+ if self.dots:
947+ if self.variable_radius:
948+ radius = data.content[2]*self.z_step
949+ cr.arc(x, self.dimensions[VERT] - y, radius, 0, 2*math.pi)
950+ cr.fill()
951+ if last_data :
952+ old_x = x0 + self.horizontal_step*last_data.content[0]
953+ old_y = y0 + self.vertical_step*last_data.content[1]
954+ cr.move_to( old_x, self.dimensions[VERT] - old_y )
955+ cr.line_to( x, self.dimensions[VERT] - y)
956+ cr.set_line_width(self.series_widths[number])
957+
958+ # Display line as dash line
959+ if self.dash and self.dash[number]:
960+ s = self.series_widths[number]
961+ cr.set_dash([s*3, s*3], 0)
962+
963+ cr.stroke()
964+ cr.set_dash([])
965+ last_data = data
966+
967+class DotLinePlot(ScatterPlot):
968+ def __init__(self,
969+ surface=None,
970+ data=None,
971+ width=640,
972+ height=480,
973+ background=None,
974+ border=0,
975+ axis = False,
976+ dash = False,
977+ dots = 0,
978+ grid = False,
979+ series_legend = False,
980+ x_labels = None,
981+ y_labels = None,
982+ x_bounds = None,
983+ y_bounds = None,
984+ x_title = None,
985+ y_title = None,
986+ series_colors = None):
987+
988+ ScatterPlot.__init__(self, surface, data, None, None, width, height, background, border,
989+ axis, dash, False, dots, grid, series_legend, x_labels, y_labels,
990+ x_bounds, y_bounds, None, x_title, y_title, series_colors, None )
991+
992+
993+ def load_series(self, data, x_labels = None, y_labels = None, series_colors=None):
994+ Plot.load_series(self, data, x_labels, y_labels, series_colors)
995+ for group in self.series :
996+ for index,data in enumerate(group):
997+ group[index].content = (index, data.content)
998+
999+ self.calc_boundaries()
1000+ self.calc_labels()
1001+
1002+class FunctionPlot(ScatterPlot):
1003+ def __init__(self,
1004+ surface=None,
1005+ data=None,
1006+ width=640,
1007+ height=480,
1008+ background=None,
1009+ border=0,
1010+ axis = False,
1011+ discrete = False,
1012+ dots = 0,
1013+ grid = False,
1014+ series_legend = False,
1015+ x_labels = None,
1016+ y_labels = None,
1017+ x_bounds = None,
1018+ y_bounds = None,
1019+ x_title = None,
1020+ y_title = None,
1021+ series_colors = None,
1022+ step = 1):
1023+
1024+ self.function = data
1025+ self.step = step
1026+ self.discrete = discrete
1027+
1028+ data, x_bounds = self.load_series_from_function( self.function, x_bounds )
1029+
1030+ ScatterPlot.__init__(self, surface, data, None, None, width, height, background, border,
1031+ axis, False, discrete, dots, grid, series_legend, x_labels, y_labels,
1032+ x_bounds, y_bounds, None, x_title, y_title, series_colors, None )
1033+
1034+ def load_series(self, data, x_labels = None, y_labels = None, series_colors=None):
1035+ Plot.load_series(self, data, x_labels, y_labels, series_colors)
1036+
1037+ if len(self.series[0][0]) is 1:
1038+ for group_id, group in enumerate(self.series) :
1039+ for index,data in enumerate(group):
1040+ group[index].content = (self.bounds[HORZ][0] + self.step*index, data.content)
1041+
1042+ self.calc_boundaries()
1043+ self.calc_labels()
1044+
1045+ def load_series_from_function( self, function, x_bounds ):
1046+ #TODO: Add the possibility for the user to define multiple functions with different discretization parameters
1047+
1048+ #This function converts a function, a list of functions or a dictionary
1049+ #of functions into its corresponding array of data
1050+ series = Series()
1051+
1052+ if isinstance(function, Group) or isinstance(function, Data):
1053+ function = Series(function)
1054+
1055+ # If is instance of Series
1056+ if isinstance(function, Series):
1057+ # Overwrite any bounds passed by the function
1058+ x_bounds = (function.range[0],function.range[-1])
1059+
1060+ #if no bounds are provided
1061+ if x_bounds == None:
1062+ x_bounds = (0,10)
1063+
1064+
1065+ #TODO: Finish the dict translation
1066+ if hasattr(function, "keys"): #dictionary:
1067+ for key in function.keys():
1068+ group = Group(name=key)
1069+ #data[ key ] = []
1070+ i = x_bounds[0]
1071+ while i <= x_bounds[1] :
1072+ group.add_data(function[ key ](i))
1073+ #data[ key ].append( function[ key ](i) )
1074+ i += self.step
1075+ series.add_group(group)
1076+
1077+ elif hasattr(function, "__delitem__"): #list of functions
1078+ for index,f in enumerate( function ) :
1079+ group = Group()
1080+ #data.append( [] )
1081+ i = x_bounds[0]
1082+ while i <= x_bounds[1] :
1083+ group.add_data(f(i))
1084+ #data[ index ].append( f(i) )
1085+ i += self.step
1086+ series.add_group(group)
1087+
1088+ elif isinstance(function, Series): # instance of Series
1089+ series = function
1090+
1091+ else: #function
1092+ group = Group()
1093+ i = x_bounds[0]
1094+ while i <= x_bounds[1] :
1095+ group.add_data(function(i))
1096+ i += self.step
1097+ series.add_group(group)
1098+
1099+
1100+ return series, x_bounds
1101+
1102+ def calc_labels(self):
1103+ if not self.labels[HORZ]:
1104+ self.labels[HORZ] = []
1105+ i = self.bounds[HORZ][0]
1106+ while i<=self.bounds[HORZ][1]:
1107+ self.labels[HORZ].append(str(i))
1108+ i += float(self.bounds[HORZ][1] - self.bounds[HORZ][0])/10
1109+ ScatterPlot.calc_labels(self)
1110+
1111+ def render_plot(self):
1112+ if not self.discrete:
1113+ ScatterPlot.render_plot(self)
1114+ else:
1115+ last = None
1116+ cr = self.context
1117+ for number, group in enumerate (self.series):
1118+ cr.set_source_rgba(*self.series_colors[number][:4])
1119+ x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step
1120+ y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step
1121+ for data in group:
1122+ x = x0 + self.horizontal_step * data.content[0]
1123+ y = y0 + self.vertical_step * data.content[1]
1124+ cr.move_to(x, self.dimensions[VERT] - y)
1125+ cr.line_to(x, self.plot_top)
1126+ cr.set_line_width(self.series_widths[number])
1127+ cr.stroke()
1128+ if self.dots:
1129+ cr.new_path()
1130+ cr.arc(x, self.dimensions[VERT] - y, 3, 0, 2.1 * math.pi)
1131+ cr.close_path()
1132+ cr.fill()
1133+
1134+class BarPlot(Plot):
1135+ def __init__(self,
1136+ surface = None,
1137+ data = None,
1138+ width = 640,
1139+ height = 480,
1140+ background = "white light_gray",
1141+ border = 0,
1142+ display_values = False,
1143+ grid = False,
1144+ rounded_corners = False,
1145+ stack = False,
1146+ three_dimension = False,
1147+ x_labels = None,
1148+ y_labels = None,
1149+ x_bounds = None,
1150+ y_bounds = None,
1151+ series_colors = None,
1152+ main_dir = None):
1153+
1154+ self.bounds = {}
1155+ self.bounds[HORZ] = x_bounds
1156+ self.bounds[VERT] = y_bounds
1157+ self.display_values = display_values
1158+ self.grid = grid
1159+ self.rounded_corners = rounded_corners
1160+ self.stack = stack
1161+ self.three_dimension = three_dimension
1162+ self.x_label_angle = math.pi / 2.5
1163+ self.main_dir = main_dir
1164+ self.max_value = {}
1165+ self.plot_dimensions = {}
1166+ self.steps = {}
1167+ self.value_label_color = (0.5,0.5,0.5,1.0)
1168+
1169+ Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors)
1170+
1171+ def load_series(self, data, x_labels = None, y_labels = None, series_colors = None):
1172+ Plot.load_series(self, data, x_labels, y_labels, series_colors)
1173+ self.calc_boundaries()
1174+
1175+ def process_colors(self, series_colors):
1176+ #Data for a BarPlot might be a List or a List of Lists.
1177+ #On the first case, colors must be generated for all bars,
1178+ #On the second, colors must be generated for each of the inner lists.
1179+
1180+ #TODO: Didn't get it...
1181+ #if hasattr(self.data[0], '__getitem__'):
1182+ # length = max(len(series) for series in self.data)
1183+ #else:
1184+ # length = len( self.data )
1185+
1186+ length = max(len(group) for group in self.series)
1187+
1188+ Plot.process_colors( self, series_colors, length, 'linear')
1189+
1190+ def calc_boundaries(self):
1191+ if not self.bounds[self.main_dir]:
1192+ if self.stack:
1193+ max_data_value = max(sum(group.to_list()) for group in self.series)
1194+ else:
1195+ max_data_value = max(max(group.to_list()) for group in self.series)
1196+ self.bounds[self.main_dir] = (0, max_data_value)
1197+ if not self.bounds[other_direction(self.main_dir)]:
1198+ self.bounds[other_direction(self.main_dir)] = (0, len(self.series))
1199+
1200+ def calc_extents(self, direction):
1201+ self.max_value[direction] = 0
1202+ if self.labels[direction]:
1203+ widest_word = max(self.labels[direction], key = lambda item: self.context.text_extents(item)[2])
1204+ self.max_value[direction] = self.context.text_extents(widest_word)[3 - direction]
1205+ self.borders[other_direction(direction)] = (2-direction)*self.max_value[direction] + self.border + direction*(5)
1206+ else:
1207+ self.borders[other_direction(direction)] = self.border
1208+
1209+ def calc_horz_extents(self):
1210+ self.calc_extents(HORZ)
1211+
1212+ def calc_vert_extents(self):
1213+ self.calc_extents(VERT)
1214+
1215+ def calc_all_extents(self):
1216+ self.calc_horz_extents()
1217+ self.calc_vert_extents()
1218+ other_dir = other_direction(self.main_dir)
1219+ self.value_label = 0
1220+ if self.display_values:
1221+ if self.stack:
1222+ self.value_label = self.context.text_extents(str(max(sum(group.to_list()) for group in self.series)))[2 + self.main_dir]
1223+ else:
1224+ self.value_label = self.context.text_extents(str(max(max(group.to_list()) for group in self.series)))[2 + self.main_dir]
1225+ if self.labels[self.main_dir]:
1226+ self.plot_dimensions[self.main_dir] = self.dimensions[self.main_dir] - 2*self.borders[self.main_dir] - self.value_label
1227+ else:
1228+ self.plot_dimensions[self.main_dir] = self.dimensions[self.main_dir] - self.borders[self.main_dir] - 1.2*self.border - self.value_label
1229+ self.plot_dimensions[other_dir] = self.dimensions[other_dir] - self.borders[other_dir] - self.border
1230+ self.plot_top = self.dimensions[VERT] - self.borders[VERT]
1231+
1232+ def calc_steps(self):
1233+ other_dir = other_direction(self.main_dir)
1234+ self.series_amplitude = self.bounds[self.main_dir][1] - self.bounds[self.main_dir][0]
1235+ if self.series_amplitude:
1236+ self.steps[self.main_dir] = float(self.plot_dimensions[self.main_dir])/self.series_amplitude
1237+ else:
1238+ self.steps[self.main_dir] = 0.00
1239+ series_length = len(self.series)
1240+ self.steps[other_dir] = float(self.plot_dimensions[other_dir])/(series_length + 0.1*(series_length + 1))
1241+ self.space = 0.1*self.steps[other_dir]
1242+
1243+ def render(self):
1244+ self.calc_all_extents()
1245+ self.calc_steps()
1246+ self.render_background()
1247+ self.render_bounding_box()
1248+ if self.grid:
1249+ self.render_grid()
1250+ if self.three_dimension:
1251+ self.render_ground()
1252+ if self.display_values:
1253+ self.render_values()
1254+ self.render_labels()
1255+ self.render_plot()
1256+ if self.series_labels:
1257+ self.render_legend()
1258+
1259+ def draw_3d_rectangle_front(self, x0, y0, x1, y1, shift):
1260+ self.context.rectangle(x0-shift, y0+shift, x1-x0, y1-y0)
1261+
1262+ def draw_3d_rectangle_side(self, x0, y0, x1, y1, shift):
1263+ self.context.move_to(x1-shift,y0+shift)
1264+ self.context.line_to(x1, y0)
1265+ self.context.line_to(x1, y1)
1266+ self.context.line_to(x1-shift, y1+shift)
1267+ self.context.line_to(x1-shift, y0+shift)
1268+ self.context.close_path()
1269+
1270+ def draw_3d_rectangle_top(self, x0, y0, x1, y1, shift):
1271+ self.context.move_to(x0-shift,y0+shift)
1272+ self.context.line_to(x0, y0)
1273+ self.context.line_to(x1, y0)
1274+ self.context.line_to(x1-shift, y0+shift)
1275+ self.context.line_to(x0-shift, y0+shift)
1276+ self.context.close_path()
1277+
1278+ def draw_round_rectangle(self, x0, y0, x1, y1):
1279+ self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2)
1280+ self.context.line_to(x1-5, y0)
1281+ self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0)
1282+ self.context.line_to(x1, y1-5)
1283+ self.context.arc(x1-5, y1-5, 5, 0, math.pi/2)
1284+ self.context.line_to(x0+5, y1)
1285+ self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi)
1286+ self.context.line_to(x0, y0+5)
1287+ self.context.close_path()
1288+
1289+ def render_ground(self):
1290+ self.draw_3d_rectangle_front(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT],
1291+ self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10)
1292+ self.context.fill()
1293+
1294+ self.draw_3d_rectangle_side (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT],
1295+ self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10)
1296+ self.context.fill()
1297+
1298+ self.draw_3d_rectangle_top (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT],
1299+ self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10)
1300+ self.context.fill()
1301+
1302+ def render_labels(self):
1303+ self.context.set_font_size(self.font_size * 0.8)
1304+ if self.labels[HORZ]:
1305+ self.render_horz_labels()
1306+ if self.labels[VERT]:
1307+ self.render_vert_labels()
1308+
1309+ def render_legend(self):
1310+ has_border = bool(getattr(self, "bar_borders", False))
1311+ cr = self.context
1312+ cr.set_font_size(self.font_size)
1313+ cr.set_line_width(self.line_width)
1314+
1315+ widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2])
1316+ tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3])
1317+ max_width = self.context.text_extents(widest_word)[2]
1318+ max_height = self.context.text_extents(tallest_word)[3] * 1.1 + 5
1319+
1320+ color_box_height = max_height / 2
1321+ color_box_width = color_box_height * 2
1322+
1323+ #Draw a bounding box
1324+ bounding_box_width = max_width + color_box_width + 15
1325+ bounding_box_height = (len(self.series_labels) + int(has_border) +0.5) * max_height
1326+ cr.set_source_rgba(1,1,1)
1327+ cr.rectangle(self.dimensions[HORZ] - self.border - bounding_box_width, self.border,
1328+ bounding_box_width, bounding_box_height)
1329+ cr.fill()
1330+
1331+ cr.set_source_rgba(*self.line_color)
1332+ cr.set_line_width(self.line_width)
1333+ cr.rectangle(self.dimensions[HORZ] - self.border - bounding_box_width, self.border,
1334+ bounding_box_width, bounding_box_height)
1335+ cr.stroke()
1336+
1337+ for idx,key in enumerate(self.series_labels):
1338+ #Draw color box
1339+ cr.set_source_rgba(*self.series_colors[idx][:4])
1340+ cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10,
1341+ self.border + color_box_height + (idx*max_height) ,
1342+ color_box_width, color_box_height)
1343+ cr.fill()
1344+
1345+ cr.set_source_rgba(0, 0, 0)
1346+ cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10,
1347+ self.border + color_box_height + (idx*max_height),
1348+ color_box_width, color_box_height)
1349+ cr.stroke()
1350+
1351+ #Draw series labels
1352+ cr.set_source_rgba(0, 0, 0)
1353+ cr.move_to(self.dimensions[HORZ] - self.border - max_width - 5, self.border + ((idx+1)*max_height))
1354+ cr.show_text(key)
1355+
1356+ if has_border:
1357+ idx += 1
1358+
1359+ cr.set_source_rgba(1, 0, 0)
1360+ cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10,
1361+ self.border + color_box_height + (idx*max_height),
1362+ color_box_width, color_box_height)
1363+ cr.stroke()
1364+
1365+ #Draw series labels
1366+ cr.set_source_rgba(0, 0, 0)
1367+ cr.move_to(self.dimensions[HORZ] - self.border - max_width - 5, self.border + ((idx+1)*max_height))
1368+ cr.show_text("is not using index")
1369+
1370+
1371+
1372+class HorizontalBarPlot(BarPlot):
1373+ def __init__(self,
1374+ surface = None,
1375+ data = None,
1376+ width = 640,
1377+ height = 480,
1378+ background = "white light_gray",
1379+ border = 0,
1380+ display_values = False,
1381+ grid = False,
1382+ rounded_corners = False,
1383+ stack = False,
1384+ three_dimension = False,
1385+ series_labels = None,
1386+ x_labels = None,
1387+ y_labels = None,
1388+ x_bounds = None,
1389+ y_bounds = None,
1390+ series_colors = None):
1391+
1392+ BarPlot.__init__(self, surface, data, width, height, background, border,
1393+ display_values, grid, rounded_corners, stack, three_dimension,
1394+ x_labels, y_labels, x_bounds, y_bounds, series_colors, HORZ)
1395+ self.series_labels = series_labels
1396+
1397+ def calc_vert_extents(self):
1398+ self.calc_extents(VERT)
1399+ if self.labels[HORZ] and not self.labels[VERT]:
1400+ self.borders[HORZ] += 10
1401+
1402+ def draw_rectangle_bottom(self, x0, y0, x1, y1):
1403+ self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi)
1404+ self.context.line_to(x0, y0+5)
1405+ self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2)
1406+ self.context.line_to(x1, y0)
1407+ self.context.line_to(x1, y1)
1408+ self.context.line_to(x0+5, y1)
1409+ self.context.close_path()
1410+
1411+ def draw_rectangle_top(self, x0, y0, x1, y1):
1412+ self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0)
1413+ self.context.line_to(x1, y1-5)
1414+ self.context.arc(x1-5, y1-5, 5, 0, math.pi/2)
1415+ self.context.line_to(x0, y1)
1416+ self.context.line_to(x0, y0)
1417+ self.context.line_to(x1, y0)
1418+ self.context.close_path()
1419+
1420+ def draw_rectangle(self, index, length, x0, y0, x1, y1):
1421+ if length == 1:
1422+ BarPlot.draw_rectangle(self, x0, y0, x1, y1)
1423+ elif index == 0:
1424+ self.draw_rectangle_bottom(x0, y0, x1, y1)
1425+ elif index == length-1:
1426+ self.draw_rectangle_top(x0, y0, x1, y1)
1427+ else:
1428+ self.context.rectangle(x0, y0, x1-x0, y1-y0)
1429+
1430+ #TODO: Review BarPlot.render_grid code
1431+ def render_grid(self):
1432+ self.context.set_source_rgba(0.8, 0.8, 0.8)
1433+ if self.labels[HORZ]:
1434+ self.context.set_font_size(self.font_size * 0.8)
1435+ step = (self.dimensions[HORZ] - 2*self.borders[HORZ] - self.value_label)/(len(self.labels[HORZ])-1)
1436+ x = self.borders[HORZ]
1437+ next_x = 0
1438+ for item in self.labels[HORZ]:
1439+ width = self.context.text_extents(item)[2]
1440+ if x - width/2 > next_x and x - width/2 > self.border:
1441+ self.context.move_to(x, self.border)
1442+ self.context.line_to(x, self.dimensions[VERT] - self.borders[VERT])
1443+ self.context.stroke()
1444+ next_x = x + width/2
1445+ x += step
1446+ else:
1447+ lines = 11
1448+ horizontal_step = float(self.plot_dimensions[HORZ])/(lines-1)
1449+ x = self.borders[HORZ]
1450+ for y in xrange(0, lines):
1451+ self.context.move_to(x, self.border)
1452+ self.context.line_to(x, self.dimensions[VERT] - self.borders[VERT])
1453+ self.context.stroke()
1454+ x += horizontal_step
1455+
1456+ def render_horz_labels(self):
1457+ step = (self.dimensions[HORZ] - 2*self.borders[HORZ])/(len(self.labels[HORZ])-1)
1458+ x = self.borders[HORZ]
1459+ next_x = 0
1460+
1461+ for item in self.labels[HORZ]:
1462+ self.context.set_source_rgba(*self.label_color)
1463+ width = self.context.text_extents(item)[2]
1464+ if x - width/2 > next_x and x - width/2 > self.border:
1465+ self.context.move_to(x - width/2, self.dimensions[VERT] - self.borders[VERT] + self.max_value[HORZ] + 3)
1466+ self.context.show_text(item)
1467+ next_x = x + width/2
1468+ x += step
1469+
1470+ def render_vert_labels(self):
1471+ series_length = len(self.labels[VERT])
1472+ step = (self.plot_dimensions[VERT] - (series_length + 1)*self.space)/(len(self.labels[VERT]))
1473+ y = self.border + step/2 + self.space
1474+
1475+ for item in self.labels[VERT]:
1476+ self.context.set_source_rgba(*self.label_color)
1477+ width, height = self.context.text_extents(item)[2:4]
1478+ self.context.move_to(self.borders[HORZ] - width - 5, y + height/2)
1479+ self.context.show_text(item)
1480+ y += step + self.space
1481+ self.labels[VERT].reverse()
1482+
1483+ def render_values(self):
1484+ self.context.set_source_rgba(*self.value_label_color)
1485+ self.context.set_font_size(self.font_size * 0.8)
1486+ if self.stack:
1487+ for i,group in enumerate(self.series):
1488+ value = sum(group.to_list())
1489+ height = self.context.text_extents(str(value))[3]
1490+ x = self.borders[HORZ] + value*self.steps[HORZ] + 2
1491+ y = self.borders[VERT] + (i+0.5)*self.steps[VERT] + (i+1)*self.space + height/2
1492+ self.context.move_to(x, y)
1493+ self.context.show_text(str(value))
1494+ else:
1495+ for i,group in enumerate(self.series):
1496+ inner_step = self.steps[VERT]/len(group)
1497+ y0 = self.border + i*self.steps[VERT] + (i+1)*self.space
1498+ for number,data in enumerate(group):
1499+ height = self.context.text_extents(str(data.content))[3]
1500+ self.context.move_to(self.borders[HORZ] + data.content*self.steps[HORZ] + 2, y0 + 0.5*inner_step + height/2, )
1501+ self.context.show_text(str(data.content))
1502+ y0 += inner_step
1503+
1504+ def render_plot(self):
1505+ if self.stack:
1506+ for i,group in enumerate(self.series):
1507+ x0 = self.borders[HORZ]
1508+ y0 = self.borders[VERT] + i*self.steps[VERT] + (i+1)*self.space
1509+ for number,data in enumerate(group):
1510+ if self.series_colors[number][4] in ('radial','linear') :
1511+ linear = cairo.LinearGradient( data.content*self.steps[HORZ]/2, y0, data.content*self.steps[HORZ]/2, y0 + self.steps[VERT] )
1512+ color = self.series_colors[number]
1513+ linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0)
1514+ linear.add_color_stop_rgba(1.0, *color[:4])
1515+ self.context.set_source(linear)
1516+ elif self.series_colors[number][4] == 'solid':
1517+ self.context.set_source_rgba(*self.series_colors[number][:4])
1518+ if self.rounded_corners:
1519+ self.draw_rectangle(number, len(group), x0, y0, x0+data.content*self.steps[HORZ], y0+self.steps[VERT])
1520+ self.context.fill()
1521+ else:
1522+ self.context.rectangle(x0, y0, data.content*self.steps[HORZ], self.steps[VERT])
1523+ self.context.fill()
1524+ x0 += data.content*self.steps[HORZ]
1525+ else:
1526+ for i,group in enumerate(self.series):
1527+ inner_step = self.steps[VERT]/len(group)
1528+ x0 = self.borders[HORZ]
1529+ y0 = self.border + i*self.steps[VERT] + (i+1)*self.space
1530+ for number,data in enumerate(group):
1531+ linear = cairo.LinearGradient(data.content*self.steps[HORZ]/2, y0, data.content*self.steps[HORZ]/2, y0 + inner_step)
1532+ color = self.series_colors[number]
1533+ linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0)
1534+ linear.add_color_stop_rgba(1.0, *color[:4])
1535+ self.context.set_source(linear)
1536+ if self.rounded_corners and data.content != 0:
1537+ BarPlot.draw_round_rectangle(self,x0, y0, x0 + data.content*self.steps[HORZ], y0 + inner_step)
1538+ self.context.fill()
1539+ else:
1540+ self.context.rectangle(x0, y0, data.content*self.steps[HORZ], inner_step)
1541+ self.context.fill()
1542+ y0 += inner_step
1543+
1544+class VerticalBarPlot(BarPlot):
1545+ def __init__(self,
1546+ surface = None,
1547+ data = None,
1548+ width = 640,
1549+ height = 480,
1550+ background = "white light_gray",
1551+ border = 0,
1552+ display_values = False,
1553+ grid = False,
1554+ rounded_corners = False,
1555+ stack = False,
1556+ three_dimension = False,
1557+ series_labels = None,
1558+ x_labels = None,
1559+ y_labels = None,
1560+ x_bounds = None,
1561+ y_bounds = None,
1562+ series_colors = None):
1563+
1564+ BarPlot.__init__(self, surface, data, width, height, background, border,
1565+ display_values, grid, rounded_corners, stack, three_dimension,
1566+ x_labels, y_labels, x_bounds, y_bounds, series_colors, VERT)
1567+ self.series_labels = series_labels
1568+
1569+ def calc_vert_extents(self):
1570+ self.calc_extents(VERT)
1571+ if self.labels[VERT] and not self.labels[HORZ]:
1572+ self.borders[VERT] += 10
1573+
1574+ def draw_rectangle_bottom(self, x0, y0, x1, y1):
1575+ self.context.move_to(x1,y1)
1576+ self.context.arc(x1-5, y1-5, 5, 0, math.pi/2)
1577+ self.context.line_to(x0+5, y1)
1578+ self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi)
1579+ self.context.line_to(x0, y0)
1580+ self.context.line_to(x1, y0)
1581+ self.context.line_to(x1, y1)
1582+ self.context.close_path()
1583+
1584+ def draw_rectangle_top(self, x0, y0, x1, y1):
1585+ self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2)
1586+ self.context.line_to(x1-5, y0)
1587+ self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0)
1588+ self.context.line_to(x1, y1)
1589+ self.context.line_to(x0, y1)
1590+ self.context.line_to(x0, y0)
1591+ self.context.close_path()
1592+
1593+ def draw_rectangle(self, index, length, x0, y0, x1, y1):
1594+ if length == 1:
1595+ BarPlot.draw_rectangle(self, x0, y0, x1, y1)
1596+ elif index == 0:
1597+ self.draw_rectangle_bottom(x0, y0, x1, y1)
1598+ elif index == length-1:
1599+ self.draw_rectangle_top(x0, y0, x1, y1)
1600+ else:
1601+ self.context.rectangle(x0, y0, x1-x0, y1-y0)
1602+
1603+ def render_grid(self):
1604+ self.context.set_source_rgba(0.8, 0.8, 0.8)
1605+ if self.labels[VERT]:
1606+ lines = len(self.labels[VERT])
1607+ vertical_step = float(self.plot_dimensions[self.main_dir])/(lines-1)
1608+ y = self.borders[VERT] + self.value_label
1609+ else:
1610+ lines = 11
1611+ vertical_step = float(self.plot_dimensions[self.main_dir])/(lines-1)
1612+ y = 1.2*self.border + self.value_label
1613+ for x in xrange(0, lines):
1614+ self.context.move_to(self.borders[HORZ], y)
1615+ self.context.line_to(self.dimensions[HORZ] - self.border, y)
1616+ self.context.stroke()
1617+ y += vertical_step
1618+
1619+ def render_ground(self):
1620+ self.draw_3d_rectangle_front(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT],
1621+ self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10)
1622+ self.context.fill()
1623+
1624+ self.draw_3d_rectangle_side (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT],
1625+ self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10)
1626+ self.context.fill()
1627+
1628+ self.draw_3d_rectangle_top (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT],
1629+ self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10)
1630+ self.context.fill()
1631+
1632+ def render_horz_labels(self):
1633+ series_length = len(self.labels[HORZ])
1634+ step = float (self.plot_dimensions[HORZ] - (series_length + 1)*self.space)/len(self.labels[HORZ])
1635+ x = self.borders[HORZ] + step/2 + self.space
1636+ next_x = 0
1637+
1638+ for item in self.labels[HORZ]:
1639+ self.context.set_source_rgba(*self.label_color)
1640+ width = self.context.text_extents(item)[2]
1641+ if not(x - width/2 > next_x and x - width/2 > self.borders[HORZ]):
1642+ items = [item[:len(item)/2], item[len(item)/2:],]
1643+ else:
1644+ items = [item,]
1645+ for i, item in enumerate(items):
1646+ width = self.context.text_extents(item)[2]
1647+ height = self.context.text_extents(item)[3]
1648+ v_space = (len(items) - i - 1) * height
1649+
1650+ if x - width/2 > next_x and x - width/2 > self.borders[HORZ]:
1651+ self.context.move_to(x - width/2, self.dimensions[VERT] - self.borders[VERT] + self.max_value[HORZ] + 3 - v_space + 3)
1652+ self.context.show_text(item)
1653+ next_x = x + width/2
1654+ x += step + self.space
1655+
1656+ def render_vert_labels(self):
1657+ self.context.set_source_rgba(*self.label_color)
1658+ y = self.borders[VERT] + self.value_label
1659+ step = (self.dimensions[VERT] - 2*self.borders[VERT] - self.value_label)/(len(self.labels[VERT]) - 1)
1660+ self.labels[VERT].reverse()
1661+ for item in self.labels[VERT]:
1662+ width, height = self.context.text_extents(item)[2:4]
1663+ self.context.move_to(self.borders[HORZ] - width - 5, y + height/2)
1664+ self.context.show_text(item)
1665+ y += step
1666+ self.labels[VERT].reverse()
1667+
1668+ def render_values(self):
1669+ self.context.set_source_rgba(*self.value_label_color)
1670+ self.context.set_font_size(self.font_size * 0.8)
1671+ if self.stack:
1672+ for i,group in enumerate(self.series):
1673+ value = sum(group.to_list())
1674+ width = self.context.text_extents(str(value))[2]
1675+ x = self.borders[HORZ] + (i+0.5)*self.steps[HORZ] + (i+1)*self.space - width/2
1676+ y = value*self.steps[VERT] + 2
1677+ self.context.move_to(x, self.plot_top-y)
1678+ self.context.show_text(str(value))
1679+ else:
1680+ for i,group in enumerate(self.series):
1681+ inner_step = self.steps[HORZ]/len(group)
1682+ x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space
1683+ for number,data in enumerate(group):
1684+ width = self.context.text_extents(str(data.content))[2]
1685+ self.context.move_to(x0 + 0.5*inner_step - width/2, self.plot_top - data.content*self.steps[VERT] - 2)
1686+ self.context.show_text(str(data.content))
1687+ x0 += inner_step
1688+
1689+ def render_plot(self):
1690+ if self.stack:
1691+ for i,group in enumerate(self.series):
1692+ x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space
1693+ y0 = 0
1694+ for number,data in enumerate(group):
1695+ if self.series_colors[number][4] in ('linear','radial'):
1696+ linear = cairo.LinearGradient( x0, data.content*self.steps[VERT]/2, x0 + self.steps[HORZ], data.content*self.steps[VERT]/2 )
1697+ color = self.series_colors[number]
1698+ linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0)
1699+ linear.add_color_stop_rgba(1.0, *color[:4])
1700+ self.context.set_source(linear)
1701+ elif self.series_colors[number][4] == 'solid':
1702+ self.context.set_source_rgba(*self.series_colors[number][:4])
1703+ if self.rounded_corners:
1704+ self.draw_rectangle(number, len(group), x0, self.plot_top - y0 - data.content*self.steps[VERT], x0 + self.steps[HORZ], self.plot_top - y0)
1705+ self.context.fill()
1706+ else:
1707+ self.context.rectangle(x0, self.plot_top - y0 - data.content*self.steps[VERT], self.steps[HORZ], data.content*self.steps[VERT])
1708+ self.context.fill()
1709+ y0 += data.content*self.steps[VERT]
1710+ else:
1711+ for i,group in enumerate(self.series):
1712+ inner_step = self.steps[HORZ]/len(group)
1713+ y0 = self.borders[VERT]
1714+ x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space
1715+ for number,data in enumerate(group):
1716+ if self.series_colors[number][4] == 'linear':
1717+ linear = cairo.LinearGradient( x0, data.content*self.steps[VERT]/2, x0 + inner_step, data.content*self.steps[VERT]/2 )
1718+ color = self.series_colors[number]
1719+ linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0)
1720+ linear.add_color_stop_rgba(1.0, *color[:4])
1721+ self.context.set_source(linear)
1722+ elif self.series_colors[number][4] == 'solid':
1723+ self.context.set_source_rgba(*self.series_colors[number][:4])
1724+ if self.rounded_corners and data.content != 0:
1725+ BarPlot.draw_round_rectangle(self, x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top)
1726+ self.context.fill()
1727+ elif self.three_dimension:
1728+ self.draw_3d_rectangle_front(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5)
1729+ self.context.fill()
1730+ self.draw_3d_rectangle_side(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5)
1731+ self.context.fill()
1732+ self.draw_3d_rectangle_top(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5)
1733+ self.context.fill()
1734+ else:
1735+ self.context.rectangle(x0, self.plot_top - data.content*self.steps[VERT], inner_step, data.content*self.steps[VERT])
1736+ if (i, number) in self.bar_borders:
1737+ self.context.fill_preserve()
1738+ self.context.set_source_rgb(1, 0, 0)
1739+ self.context.stroke()
1740+ else:
1741+ self.context.fill()
1742+
1743+ x0 += inner_step
1744+
1745+class StreamChart(VerticalBarPlot):
1746+ def __init__(self,
1747+ surface = None,
1748+ data = None,
1749+ width = 640,
1750+ height = 480,
1751+ background = "white light_gray",
1752+ border = 0,
1753+ grid = False,
1754+ series_legend = None,
1755+ x_labels = None,
1756+ x_bounds = None,
1757+ y_bounds = None,
1758+ series_colors = None):
1759+
1760+ VerticalBarPlot.__init__(self, surface, data, width, height, background, border,
1761+ False, grid, False, True, False,
1762+ None, x_labels, None, x_bounds, y_bounds, series_colors)
1763+
1764+ def calc_steps(self):
1765+ other_dir = other_direction(self.main_dir)
1766+ self.series_amplitude = self.bounds[self.main_dir][1] - self.bounds[self.main_dir][0]
1767+ if self.series_amplitude:
1768+ self.steps[self.main_dir] = float(self.plot_dimensions[self.main_dir])/self.series_amplitude
1769+ else:
1770+ self.steps[self.main_dir] = 0.00
1771+ series_length = len(self.data)
1772+ self.steps[other_dir] = float(self.plot_dimensions[other_dir])/series_length
1773+
1774+ def render_legend(self):
1775+ pass
1776+
1777+ def ground(self, index):
1778+ sum_values = sum(self.data[index])
1779+ return -0.5*sum_values
1780+
1781+ def calc_angles(self):
1782+ middle = self.plot_top - self.plot_dimensions[VERT]/2.0
1783+ self.angles = [tuple([0.0 for x in range(len(self.data)+1)])]
1784+ for x_index in range(1, len(self.data)-1):
1785+ t = []
1786+ x0 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ]
1787+ x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ]
1788+ y0 = middle - self.ground(x_index-1)*self.steps[VERT]
1789+ y2 = middle - self.ground(x_index+1)*self.steps[VERT]
1790+ t.append(math.atan(float(y0-y2)/(x0-x2)))
1791+ for data_index in range(len(self.data[x_index])):
1792+ x0 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ]
1793+ x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ]
1794+ y0 = middle - self.ground(x_index-1)*self.steps[VERT] - self.data[x_index-1][data_index]*self.steps[VERT]
1795+ y2 = middle - self.ground(x_index+1)*self.steps[VERT] - self.data[x_index+1][data_index]*self.steps[VERT]
1796+
1797+ for i in range(0,data_index):
1798+ y0 -= self.data[x_index-1][i]*self.steps[VERT]
1799+ y2 -= self.data[x_index+1][i]*self.steps[VERT]
1800+
1801+ if data_index == len(self.data[0])-1 and False:
1802+ self.context.set_source_rgba(0.0,0.0,0.0,0.3)
1803+ self.context.move_to(x0,y0)
1804+ self.context.line_to(x2,y2)
1805+ self.context.stroke()
1806+ self.context.arc(x0,y0,2,0,2*math.pi)
1807+ self.context.fill()
1808+ t.append(math.atan(float(y0-y2)/(x0-x2)))
1809+ self.angles.append(tuple(t))
1810+ self.angles.append(tuple([0.0 for x in range(len(self.data)+1)]))
1811+
1812+ def render_plot(self):
1813+ self.calc_angles()
1814+ middle = self.plot_top - self.plot_dimensions[VERT]/2.0
1815+ p = 0.4*self.steps[HORZ]
1816+ for data_index in range(len(self.data[0])-1,-1,-1):
1817+ self.context.set_source_rgba(*self.series_colors[data_index][:4])
1818+
1819+ #draw the upper line
1820+ for x_index in range(len(self.data)-1) :
1821+ x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ]
1822+ y1 = middle - self.ground(x_index)*self.steps[VERT] - self.data[x_index][data_index]*self.steps[VERT]
1823+ x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ]
1824+ y2 = middle - self.ground(x_index + 1)*self.steps[VERT] - self.data[x_index + 1][data_index]*self.steps[VERT]
1825+
1826+ for i in range(0,data_index):
1827+ y1 -= self.data[x_index][i]*self.steps[VERT]
1828+ y2 -= self.data[x_index+1][i]*self.steps[VERT]
1829+
1830+ if x_index == 0:
1831+ self.context.move_to(x1,y1)
1832+
1833+ ang1 = self.angles[x_index][data_index+1]
1834+ ang2 = self.angles[x_index+1][data_index+1] + math.pi
1835+ self.context.curve_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1),
1836+ x2+p*math.cos(ang2),y2+p*math.sin(ang2),
1837+ x2,y2)
1838+
1839+ for x_index in range(len(self.data)-1,0,-1) :
1840+ x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ]
1841+ y1 = middle - self.ground(x_index)*self.steps[VERT]
1842+ x2 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ]
1843+ y2 = middle - self.ground(x_index - 1)*self.steps[VERT]
1844+
1845+ for i in range(0,data_index):
1846+ y1 -= self.data[x_index][i]*self.steps[VERT]
1847+ y2 -= self.data[x_index-1][i]*self.steps[VERT]
1848+
1849+ if x_index == len(self.data)-1:
1850+ self.context.line_to(x1,y1+2)
1851+
1852+ #revert angles by pi degrees to take the turn back
1853+ ang1 = self.angles[x_index][data_index] + math.pi
1854+ ang2 = self.angles[x_index-1][data_index]
1855+ self.context.curve_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1),
1856+ x2+p*math.cos(ang2),y2+p*math.sin(ang2),
1857+ x2,y2+2)
1858+
1859+ self.context.close_path()
1860+ self.context.fill()
1861+
1862+ if False:
1863+ self.context.move_to(self.borders[HORZ] + 0.5*self.steps[HORZ], middle)
1864+ for x_index in range(len(self.data)-1) :
1865+ x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ]
1866+ y1 = middle - self.ground(x_index)*self.steps[VERT] - self.data[x_index][data_index]*self.steps[VERT]
1867+ x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ]
1868+ y2 = middle - self.ground(x_index + 1)*self.steps[VERT] - self.data[x_index + 1][data_index]*self.steps[VERT]
1869+
1870+ for i in range(0,data_index):
1871+ y1 -= self.data[x_index][i]*self.steps[VERT]
1872+ y2 -= self.data[x_index+1][i]*self.steps[VERT]
1873+
1874+ ang1 = self.angles[x_index][data_index+1]
1875+ ang2 = self.angles[x_index+1][data_index+1] + math.pi
1876+ self.context.set_source_rgba(1.0,0.0,0.0)
1877+ self.context.arc(x1+p*math.cos(ang1),y1+p*math.sin(ang1),2,0,2*math.pi)
1878+ self.context.fill()
1879+ self.context.set_source_rgba(0.0,0.0,0.0)
1880+ self.context.arc(x2+p*math.cos(ang2),y2+p*math.sin(ang2),2,0,2*math.pi)
1881+ self.context.fill()
1882+ '''self.context.set_source_rgba(0.0,0.0,0.0,0.3)
1883+ self.context.arc(x2,y2,2,0,2*math.pi)
1884+ self.context.fill()'''
1885+ self.context.move_to(x1,y1)
1886+ self.context.line_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1))
1887+ self.context.stroke()
1888+ self.context.move_to(x2,y2)
1889+ self.context.line_to(x2+p*math.cos(ang2),y2+p*math.sin(ang2))
1890+ self.context.stroke()
1891+ if False:
1892+ for x_index in range(len(self.data)-1,0,-1) :
1893+ x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ]
1894+ y1 = middle - self.ground(x_index)*self.steps[VERT]
1895+ x2 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ]
1896+ y2 = middle - self.ground(x_index - 1)*self.steps[VERT]
1897+
1898+ for i in range(0,data_index):
1899+ y1 -= self.data[x_index][i]*self.steps[VERT]
1900+ y2 -= self.data[x_index-1][i]*self.steps[VERT]
1901+
1902+ #revert angles by pi degrees to take the turn back
1903+ ang1 = self.angles[x_index][data_index] + math.pi
1904+ ang2 = self.angles[x_index-1][data_index]
1905+ self.context.set_source_rgba(0.0,1.0,0.0)
1906+ self.context.arc(x1+p*math.cos(ang1),y1+p*math.sin(ang1),2,0,2*math.pi)
1907+ self.context.fill()
1908+ self.context.set_source_rgba(0.0,0.0,1.0)
1909+ self.context.arc(x2+p*math.cos(ang2),y2+p*math.sin(ang2),2,0,2*math.pi)
1910+ self.context.fill()
1911+ '''self.context.set_source_rgba(0.0,0.0,0.0,0.3)
1912+ self.context.arc(x2,y2,2,0,2*math.pi)
1913+ self.context.fill()'''
1914+ self.context.move_to(x1,y1)
1915+ self.context.line_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1))
1916+ self.context.stroke()
1917+ self.context.move_to(x2,y2)
1918+ self.context.line_to(x2+p*math.cos(ang2),y2+p*math.sin(ang2))
1919+ self.context.stroke()
1920+ #break
1921+
1922+ #self.context.arc(self.dimensions[HORZ]/2, self.dimensions[VERT]/2,50,0,3*math.pi/2)
1923+ #self.context.fill()
1924+
1925+
1926+class PiePlot(Plot):
1927+ #TODO: Check the old cairoplot, graphs aren't matching
1928+ def __init__ (self,
1929+ surface = None,
1930+ data = None,
1931+ width = 640,
1932+ height = 480,
1933+ background = "white light_gray",
1934+ gradient = False,
1935+ shadow = False,
1936+ colors = None):
1937+
1938+ Plot.__init__( self, surface, data, width, height, background, series_colors = colors )
1939+ self.center = (self.dimensions[HORZ]/2, self.dimensions[VERT]/2)
1940+ self.total = sum( self.series.to_list() )
1941+ self.radius = min(self.dimensions[HORZ]/3,self.dimensions[VERT]/3)
1942+ self.gradient = gradient
1943+ self.shadow = shadow
1944+
1945+ def sort_function(x,y):
1946+ return x.content - y.content
1947+
1948+ def load_series(self, data, x_labels=None, y_labels=None, series_colors=None):
1949+ Plot.load_series(self, data, x_labels, y_labels, series_colors)
1950+ # Already done inside series
1951+ #self.data = sorted(self.data)
1952+
1953+ def draw_piece(self, angle, next_angle):
1954+ self.context.move_to(self.center[0],self.center[1])
1955+ self.context.line_to(self.center[0] + self.radius*math.cos(angle), self.center[1] + self.radius*math.sin(angle))
1956+ self.context.arc(self.center[0], self.center[1], self.radius, angle, next_angle)
1957+ self.context.line_to(self.center[0], self.center[1])
1958+ self.context.close_path()
1959+
1960+ def render(self):
1961+ self.render_background()
1962+ self.render_bounding_box()
1963+ if self.shadow:
1964+ self.render_shadow()
1965+ self.render_plot()
1966+ self.render_series_labels()
1967+
1968+ def render_shadow(self):
1969+ horizontal_shift = 3
1970+ vertical_shift = 3
1971+ self.context.set_source_rgba(0, 0, 0, 0.5)
1972+ self.context.arc(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.radius, 0, 2*math.pi)
1973+ self.context.fill()
1974+
1975+ def render_series_labels(self):
1976+ angle = 0
1977+ next_angle = 0
1978+ x0,y0 = self.center
1979+ cr = self.context
1980+ for number,key in enumerate(self.series_labels):
1981+ # self.data[number] should be just a number
1982+ data = sum(self.series[number].to_list())
1983+
1984+ next_angle = angle + 2.0*math.pi*data/self.total
1985+ cr.set_source_rgba(*self.series_colors[number][:4])
1986+ w = cr.text_extents(key)[2]
1987+ if (angle + next_angle)/2 < math.pi/2 or (angle + next_angle)/2 > 3*math.pi/2:
1988+ cr.move_to(x0 + (self.radius+10)*math.cos((angle+next_angle)/2), y0 + (self.radius+10)*math.sin((angle+next_angle)/2) )
1989+ else:
1990+ cr.move_to(x0 + (self.radius+10)*math.cos((angle+next_angle)/2) - w, y0 + (self.radius+10)*math.sin((angle+next_angle)/2) )
1991+ cr.show_text(key)
1992+ angle = next_angle
1993+
1994+ def render_plot(self):
1995+ angle = 0
1996+ next_angle = 0
1997+ x0,y0 = self.center
1998+ cr = self.context
1999+ for number,group in enumerate(self.series):
2000+ # Group should be just a number
2001+ data = sum(group.to_list())
2002+ next_angle = angle + 2.0*math.pi*data/self.total
2003+ if self.gradient or self.series_colors[number][4] in ('linear','radial'):
2004+ gradient_color = cairo.RadialGradient(self.center[0], self.center[1], 0, self.center[0], self.center[1], self.radius)
2005+ gradient_color.add_color_stop_rgba(0.3, *self.series_colors[number][:4])
2006+ gradient_color.add_color_stop_rgba(1, self.series_colors[number][0]*0.7,
2007+ self.series_colors[number][1]*0.7,
2008+ self.series_colors[number][2]*0.7,
2009+ self.series_colors[number][3])
2010+ cr.set_source(gradient_color)
2011+ else:
2012+ cr.set_source_rgba(*self.series_colors[number][:4])
2013+
2014+ self.draw_piece(angle, next_angle)
2015+ cr.fill()
2016+
2017+ cr.set_source_rgba(1.0, 1.0, 1.0)
2018+ self.draw_piece(angle, next_angle)
2019+ cr.stroke()
2020+
2021+ angle = next_angle
2022+
2023+class DonutPlot(PiePlot):
2024+ def __init__ (self,
2025+ surface = None,
2026+ data = None,
2027+ width = 640,
2028+ height = 480,
2029+ background = "white light_gray",
2030+ gradient = False,
2031+ shadow = False,
2032+ colors = None,
2033+ inner_radius=-1):
2034+
2035+ Plot.__init__( self, surface, data, width, height, background, series_colors = colors )
2036+
2037+ self.center = ( self.dimensions[HORZ]/2, self.dimensions[VERT]/2 )
2038+ self.total = sum( self.series.to_list() )
2039+ self.radius = min( self.dimensions[HORZ]/3,self.dimensions[VERT]/3 )
2040+ self.inner_radius = inner_radius*self.radius
2041+
2042+ if inner_radius == -1:
2043+ self.inner_radius = self.radius/3
2044+
2045+ self.gradient = gradient
2046+ self.shadow = shadow
2047+
2048+ def draw_piece(self, angle, next_angle):
2049+ self.context.move_to(self.center[0] + (self.inner_radius)*math.cos(angle), self.center[1] + (self.inner_radius)*math.sin(angle))
2050+ self.context.line_to(self.center[0] + self.radius*math.cos(angle), self.center[1] + self.radius*math.sin(angle))
2051+ self.context.arc(self.center[0], self.center[1], self.radius, angle, next_angle)
2052+ self.context.line_to(self.center[0] + (self.inner_radius)*math.cos(next_angle), self.center[1] + (self.inner_radius)*math.sin(next_angle))
2053+ self.context.arc_negative(self.center[0], self.center[1], self.inner_radius, next_angle, angle)
2054+ self.context.close_path()
2055+
2056+ def render_shadow(self):
2057+ horizontal_shift = 3
2058+ vertical_shift = 3
2059+ self.context.set_source_rgba(0, 0, 0, 0.5)
2060+ self.context.arc(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.inner_radius, 0, 2*math.pi)
2061+ self.context.arc_negative(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.radius, 0, -2*math.pi)
2062+ self.context.fill()
2063+
2064+class GanttChart (Plot) :
2065+ def __init__(self,
2066+ surface = None,
2067+ data = None,
2068+ width = 640,
2069+ height = 480,
2070+ x_labels = None,
2071+ y_labels = None,
2072+ colors = None):
2073+ self.bounds = {}
2074+ self.max_value = {}
2075+ Plot.__init__(self, surface, data, width, height, x_labels = x_labels, y_labels = y_labels, series_colors = colors)
2076+
2077+ def load_series(self, data, x_labels=None, y_labels=None, series_colors=None):
2078+ Plot.load_series(self, data, x_labels, y_labels, series_colors)
2079+ self.calc_boundaries()
2080+
2081+ def calc_boundaries(self):
2082+ self.bounds[HORZ] = (0,len(self.series))
2083+ end_pos = max(self.series.to_list())
2084+
2085+ #for group in self.series:
2086+ # if hasattr(item, "__delitem__"):
2087+ # for sub_item in item:
2088+ # end_pos = max(sub_item)
2089+ # else:
2090+ # end_pos = max(item)
2091+ self.bounds[VERT] = (0,end_pos)
2092+
2093+ def calc_extents(self, direction):
2094+ self.max_value[direction] = 0
2095+ if self.labels[direction]:
2096+ self.max_value[direction] = max(self.context.text_extents(item)[2] for item in self.labels[direction])
2097+ else:
2098+ self.max_value[direction] = self.context.text_extents( str(self.bounds[direction][1] + 1) )[2]
2099+
2100+ def calc_horz_extents(self):
2101+ self.calc_extents(HORZ)
2102+ self.borders[HORZ] = 100 + self.max_value[HORZ]
2103+
2104+ def calc_vert_extents(self):
2105+ self.calc_extents(VERT)
2106+ self.borders[VERT] = self.dimensions[VERT]/(self.bounds[HORZ][1] + 1)
2107+
2108+ def calc_steps(self):
2109+ self.horizontal_step = (self.dimensions[HORZ] - self.borders[HORZ])/(len(self.labels[VERT]))
2110+ self.vertical_step = self.borders[VERT]
2111+
2112+ def render(self):
2113+ self.calc_horz_extents()
2114+ self.calc_vert_extents()
2115+ self.calc_steps()
2116+ self.render_background()
2117+
2118+ self.render_labels()
2119+ self.render_grid()
2120+ self.render_plot()
2121+
2122+ def render_background(self):
2123+ cr = self.context
2124+ cr.set_source_rgba(255,255,255)
2125+ cr.rectangle(0,0,self.dimensions[HORZ], self.dimensions[VERT])
2126+ cr.fill()
2127+ for number,group in enumerate(self.series):
2128+ linear = cairo.LinearGradient(self.dimensions[HORZ]/2, self.borders[VERT] + number*self.vertical_step,
2129+ self.dimensions[HORZ]/2, self.borders[VERT] + (number+1)*self.vertical_step)
2130+ linear.add_color_stop_rgba(0,1.0,1.0,1.0,1.0)
2131+ linear.add_color_stop_rgba(1.0,0.9,0.9,0.9,1.0)
2132+ cr.set_source(linear)
2133+ cr.rectangle(0,self.borders[VERT] + number*self.vertical_step,self.dimensions[HORZ],self.vertical_step)
2134+ cr.fill()
2135+
2136+ def render_grid(self):
2137+ cr = self.context
2138+ cr.set_source_rgba(0.7, 0.7, 0.7)
2139+ cr.set_dash((1,0,0,0,0,0,1))
2140+ cr.set_line_width(0.5)
2141+ for number,label in enumerate(self.labels[VERT]):
2142+ h = cr.text_extents(label)[3]
2143+ cr.move_to(self.borders[HORZ] + number*self.horizontal_step, self.vertical_step/2 + h)
2144+ cr.line_to(self.borders[HORZ] + number*self.horizontal_step, self.dimensions[VERT])
2145+ cr.stroke()
2146+
2147+ def render_labels(self):
2148+ self.context.set_font_size(0.02 * self.dimensions[HORZ])
2149+
2150+ self.render_horz_labels()
2151+ self.render_vert_labels()
2152+
2153+ def render_horz_labels(self):
2154+ cr = self.context
2155+ labels = self.labels[HORZ]
2156+ if not labels:
2157+ labels = [str(i) for i in range(1, self.bounds[HORZ][1] + 1) ]
2158+ for number,label in enumerate(labels):
2159+ if label != None:
2160+ cr.set_source_rgba(0.5, 0.5, 0.5)
2161+ w,h = cr.text_extents(label)[2], cr.text_extents(label)[3]
2162+ cr.move_to(40,self.borders[VERT] + number*self.vertical_step + self.vertical_step/2 + h/2)
2163+ cr.show_text(label)
2164+
2165+ def render_vert_labels(self):
2166+ cr = self.context
2167+ labels = self.labels[VERT]
2168+ if not labels:
2169+ labels = [str(i) for i in range(1, self.bounds[VERT][1] + 1) ]
2170+ for number,label in enumerate(labels):
2171+ w,h = cr.text_extents(label)[2], cr.text_extents(label)[3]
2172+ cr.move_to(self.borders[HORZ] + number*self.horizontal_step - w/2, self.vertical_step/2)
2173+ cr.show_text(label)
2174+
2175+ def render_rectangle(self, x0, y0, x1, y1, color):
2176+ self.draw_shadow(x0, y0, x1, y1)
2177+ self.draw_rectangle(x0, y0, x1, y1, color)
2178+
2179+ def draw_rectangular_shadow(self, gradient, x0, y0, w, h):
2180+ self.context.set_source(gradient)
2181+ self.context.rectangle(x0,y0,w,h)
2182+ self.context.fill()
2183+
2184+ def draw_circular_shadow(self, x, y, radius, ang_start, ang_end, mult, shadow):
2185+ gradient = cairo.RadialGradient(x, y, 0, x, y, 2*radius)
2186+ gradient.add_color_stop_rgba(0, 0, 0, 0, shadow)
2187+ gradient.add_color_stop_rgba(1, 0, 0, 0, 0)
2188+ self.context.set_source(gradient)
2189+ self.context.move_to(x,y)
2190+ self.context.line_to(x + mult[0]*radius,y + mult[1]*radius)
2191+ self.context.arc(x, y, 8, ang_start, ang_end)
2192+ self.context.line_to(x,y)
2193+ self.context.close_path()
2194+ self.context.fill()
2195+
2196+ def draw_rectangle(self, x0, y0, x1, y1, color):
2197+ cr = self.context
2198+ middle = (x0+x1)/2
2199+ linear = cairo.LinearGradient(middle,y0,middle,y1)
2200+ linear.add_color_stop_rgba(0,3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0)
2201+ linear.add_color_stop_rgba(1,*color[:4])
2202+ cr.set_source(linear)
2203+
2204+ cr.arc(x0+5, y0+5, 5, 0, 2*math.pi)
2205+ cr.arc(x1-5, y0+5, 5, 0, 2*math.pi)
2206+ cr.arc(x0+5, y1-5, 5, 0, 2*math.pi)
2207+ cr.arc(x1-5, y1-5, 5, 0, 2*math.pi)
2208+ cr.rectangle(x0+5,y0,x1-x0-10,y1-y0)
2209+ cr.rectangle(x0,y0+5,x1-x0,y1-y0-10)
2210+ cr.fill()
2211+
2212+ def draw_shadow(self, x0, y0, x1, y1):
2213+ shadow = 0.4
2214+ h_mid = (x0+x1)/2
2215+ v_mid = (y0+y1)/2
2216+ h_linear_1 = cairo.LinearGradient(h_mid,y0-4,h_mid,y0+4)
2217+ h_linear_2 = cairo.LinearGradient(h_mid,y1-4,h_mid,y1+4)
2218+ v_linear_1 = cairo.LinearGradient(x0-4,v_mid,x0+4,v_mid)
2219+ v_linear_2 = cairo.LinearGradient(x1-4,v_mid,x1+4,v_mid)
2220+
2221+ h_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0)
2222+ h_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow)
2223+ h_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow)
2224+ h_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0)
2225+ v_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0)
2226+ v_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow)
2227+ v_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow)
2228+ v_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0)
2229+
2230+ self.draw_rectangular_shadow(h_linear_1,x0+4,y0-4,x1-x0-8,8)
2231+ self.draw_rectangular_shadow(h_linear_2,x0+4,y1-4,x1-x0-8,8)
2232+ self.draw_rectangular_shadow(v_linear_1,x0-4,y0+4,8,y1-y0-8)
2233+ self.draw_rectangular_shadow(v_linear_2,x1-4,y0+4,8,y1-y0-8)
2234+
2235+ self.draw_circular_shadow(x0+4, y0+4, 4, math.pi, 3*math.pi/2, (-1,0), shadow)
2236+ self.draw_circular_shadow(x1-4, y0+4, 4, 3*math.pi/2, 2*math.pi, (0,-1), shadow)
2237+ self.draw_circular_shadow(x0+4, y1-4, 4, math.pi/2, math.pi, (0,1), shadow)
2238+ self.draw_circular_shadow(x1-4, y1-4, 4, 0, math.pi/2, (1,0), shadow)
2239+
2240+ def render_plot(self):
2241+ for index,group in enumerate(self.series):
2242+ for data in group:
2243+ self.render_rectangle(self.borders[HORZ] + data.content[0]*self.horizontal_step,
2244+ self.borders[VERT] + index*self.vertical_step + self.vertical_step/4.0,
2245+ self.borders[HORZ] + data.content[1]*self.horizontal_step,
2246+ self.borders[VERT] + index*self.vertical_step + 3.0*self.vertical_step/4.0,
2247+ self.series_colors[index])
2248+
2249+# Function definition
2250+
2251+def scatter_plot(name,
2252+ data = None,
2253+ errorx = None,
2254+ errory = None,
2255+ width = 640,
2256+ height = 480,
2257+ background = "white light_gray",
2258+ border = 0,
2259+ axis = False,
2260+ dash = False,
2261+ discrete = False,
2262+ dots = False,
2263+ grid = False,
2264+ series_legend = False,
2265+ x_labels = None,
2266+ y_labels = None,
2267+ x_bounds = None,
2268+ y_bounds = None,
2269+ z_bounds = None,
2270+ x_title = None,
2271+ y_title = None,
2272+ series_colors = None,
2273+ circle_colors = None):
2274+
2275+ '''
2276+ - Function to plot scatter data.
2277+
2278+ - Parameters
2279+
2280+ data - The values to be ploted might be passed in a two basic:
2281+ list of points: [(0,0), (0,1), (0,2)] or [(0,0,1), (0,1,4), (0,2,1)]
2282+ lists of coordinates: [ [0,0,0] , [0,1,2] ] or [ [0,0,0] , [0,1,2] , [1,4,1] ]
2283+ Notice that these kinds of that can be grouped in order to form more complex data
2284+ using lists of lists or dictionaries;
2285+ series_colors - Define color values for each of the series
2286+ circle_colors - Define a lower and an upper bound for the circle colors for variable radius
2287+ (3 dimensions) series
2288+ '''
2289+
2290+ plot = ScatterPlot( name, data, errorx, errory, width, height, background, border,
2291+ axis, dash, discrete, dots, grid, series_legend, x_labels, y_labels,
2292+ x_bounds, y_bounds, z_bounds, x_title, y_title, series_colors, circle_colors )
2293+ plot.render()
2294+ plot.commit()
2295+
2296+def dot_line_plot(name,
2297+ data,
2298+ width,
2299+ height,
2300+ background = "white light_gray",
2301+ border = 0,
2302+ axis = False,
2303+ dash = False,
2304+ dots = False,
2305+ grid = False,
2306+ series_legend = False,
2307+ x_labels = None,
2308+ y_labels = None,
2309+ x_bounds = None,
2310+ y_bounds = None,
2311+ x_title = None,
2312+ y_title = None,
2313+ series_colors = None):
2314+ '''
2315+ - Function to plot graphics using dots and lines.
2316+
2317+ dot_line_plot (name, data, width, height, background = "white light_gray", border = 0, axis = False, grid = False, x_labels = None, y_labels = None, x_bounds = None, y_bounds = None)
2318+
2319+ - Parameters
2320+
2321+ name - Name of the desired output file, no need to input the .svg as it will be added at runtim;
2322+ data - The list, list of lists or dictionary holding the data to be plotted;
2323+ width, height - Dimensions of the output image;
2324+ background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient.
2325+ If left None, a gray to white gradient will be generated;
2326+ border - Distance in pixels of a square border into which the graphics will be drawn;
2327+ axis - Whether or not the axis are to be drawn;
2328+ dash - Boolean or a list or a dictionary of booleans indicating whether or not the associated series should be drawn in dashed mode;
2329+ dots - Whether or not dots should be drawn on each point;
2330+ grid - Whether or not the gris is to be drawn;
2331+ series_legend - Whether or not the legend is to be drawn;
2332+ x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis;
2333+ x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted;
2334+ x_title - Whether or not to plot a title over the x axis.
2335+ y_title - Whether or not to plot a title over the y axis.
2336+
2337+ - Examples of use
2338+
2339+ data = [0, 1, 3, 8, 9, 0, 10, 10, 2, 1]
2340+ CairoPlot.dot_line_plot('teste', data, 400, 300)
2341+
2342+ data = { "john" : [10, 10, 10, 10, 30], "mary" : [0, 0, 3, 5, 15], "philip" : [13, 32, 11, 25, 2] }
2343+ x_labels = ["jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008" ]
2344+ CairoPlot.dot_line_plot( 'test', data, 400, 300, axis = True, grid = True,
2345+ series_legend = True, x_labels = x_labels )
2346+ '''
2347+ plot = DotLinePlot( name, data, width, height, background, border,
2348+ axis, dash, dots, grid, series_legend, x_labels, y_labels,
2349+ x_bounds, y_bounds, x_title, y_title, series_colors )
2350+ plot.render()
2351+ plot.commit()
2352+
2353+def function_plot(name,
2354+ data,
2355+ width,
2356+ height,
2357+ background = "white light_gray",
2358+ border = 0,
2359+ axis = True,
2360+ dots = False,
2361+ discrete = False,
2362+ grid = False,
2363+ series_legend = False,
2364+ x_labels = None,
2365+ y_labels = None,
2366+ x_bounds = None,
2367+ y_bounds = None,
2368+ x_title = None,
2369+ y_title = None,
2370+ series_colors = None,
2371+ step = 1):
2372+
2373+ '''
2374+ - Function to plot functions.
2375+
2376+ function_plot(name, data, width, height, background = "white light_gray", border = 0, axis = True, grid = False, dots = False, x_labels = None, y_labels = None, x_bounds = None, y_bounds = None, step = 1, discrete = False)
2377+
2378+ - Parameters
2379+
2380+ name - Name of the desired output file, no need to input the .svg as it will be added at runtim;
2381+ data - The list, list of lists or dictionary holding the data to be plotted;
2382+ width, height - Dimensions of the output image;
2383+ background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient.
2384+ If left None, a gray to white gradient will be generated;
2385+ border - Distance in pixels of a square border into which the graphics will be drawn;
2386+ axis - Whether or not the axis are to be drawn;
2387+ grid - Whether or not the gris is to be drawn;
2388+ dots - Whether or not dots should be shown at each point;
2389+ x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis;
2390+ x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted;
2391+ step - the horizontal distance from one point to the other. The smaller, the smoother the curve will be;
2392+ discrete - whether or not the function should be plotted in discrete format.
2393+
2394+ - Example of use
2395+
2396+ data = lambda x : x**2
2397+ CairoPlot.function_plot('function4', data, 400, 300, grid = True, x_bounds=(-10,10), step = 0.1)
2398+ '''
2399+
2400+ plot = FunctionPlot( name, data, width, height, background, border,
2401+ axis, discrete, dots, grid, series_legend, x_labels, y_labels,
2402+ x_bounds, y_bounds, x_title, y_title, series_colors, step )
2403+ plot.render()
2404+ plot.commit()
2405+
2406+def pie_plot( name, data, width, height, background = "white light_gray", gradient = False, shadow = False, colors = None ):
2407+
2408+ '''
2409+ - Function to plot pie graphics.
2410+
2411+ pie_plot(name, data, width, height, background = "white light_gray", gradient = False, colors = None)
2412+
2413+ - Parameters
2414+
2415+ name - Name of the desired output file, no need to input the .svg as it will be added at runtim;
2416+ data - The list, list of lists or dictionary holding the data to be plotted;
2417+ width, height - Dimensions of the output image;
2418+ background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient.
2419+ If left None, a gray to white gradient will be generated;
2420+ gradient - Whether or not the pie color will be painted with a gradient;
2421+ shadow - Whether or not there will be a shadow behind the pie;
2422+ colors - List of slices colors.
2423+
2424+ - Example of use
2425+
2426+ teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235}
2427+ CairoPlot.pie_plot("pie_teste", teste_data, 500, 500)
2428+ '''
2429+
2430+ plot = PiePlot( name, data, width, height, background, gradient, shadow, colors )
2431+ plot.render()
2432+ plot.commit()
2433+
2434+def donut_plot(name, data, width, height, background = "white light_gray", gradient = False, shadow = False, colors = None, inner_radius = -1):
2435+
2436+ '''
2437+ - Function to plot donut graphics.
2438+
2439+ donut_plot(name, data, width, height, background = "white light_gray", gradient = False, inner_radius = -1)
2440+
2441+ - Parameters
2442+
2443+ name - Name of the desired output file, no need to input the .svg as it will be added at runtim;
2444+ data - The list, list of lists or dictionary holding the data to be plotted;
2445+ width, height - Dimensions of the output image;
2446+ background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient.
2447+ If left None, a gray to white gradient will be generated;
2448+ shadow - Whether or not there will be a shadow behind the donut;
2449+ gradient - Whether or not the donut color will be painted with a gradient;
2450+ colors - List of slices colors;
2451+ inner_radius - The radius of the donut's inner circle.
2452+
2453+ - Example of use
2454+
2455+ teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235}
2456+ CairoPlot.donut_plot("donut_teste", teste_data, 500, 500)
2457+ '''
2458+
2459+ plot = DonutPlot(name, data, width, height, background, gradient, shadow, colors, inner_radius)
2460+ plot.render()
2461+ plot.commit()
2462+
2463+def gantt_chart(name, pieces, width, height, x_labels, y_labels, colors):
2464+
2465+ '''
2466+ - Function to generate Gantt Charts.
2467+
2468+ gantt_chart(name, pieces, width, height, x_labels, y_labels, colors):
2469+
2470+ - Parameters
2471+
2472+ name - Name of the desired output file, no need to input the .svg as it will be added at runtim;
2473+ pieces - A list defining the spaces to be drawn. The user must pass, for each line, the index of its start and the index of its end. If a line must have two or more spaces, they must be passed inside a list;
2474+ width, height - Dimensions of the output image;
2475+ x_labels - A list of names for each of the vertical lines;
2476+ y_labels - A list of names for each of the horizontal spaces;
2477+ colors - List containing the colors expected for each of the horizontal spaces
2478+
2479+ - Example of use
2480+
2481+ pieces = [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,8)]
2482+ x_labels = [ 'teste01', 'teste02', 'teste03', 'teste04']
2483+ y_labels = [ '0001', '0002', '0003', '0004', '0005', '0006', '0007', '0008', '0009', '0010' ]
2484+ colors = [ (1.0, 0.0, 0.0), (1.0, 0.7, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0) ]
2485+ CairoPlot.gantt_chart('gantt_teste', pieces, 600, 300, x_labels, y_labels, colors)
2486+ '''
2487+
2488+ plot = GanttChart(name, pieces, width, height, x_labels, y_labels, colors)
2489+ plot.render()
2490+ plot.commit()
2491+
2492+def vertical_bar_plot(name,
2493+ data,
2494+ width,
2495+ height,
2496+ background = "white light_gray",
2497+ border = 0,
2498+ display_values = False,
2499+ grid = False,
2500+ rounded_corners = False,
2501+ stack = False,
2502+ three_dimension = False,
2503+ series_labels = None,
2504+ x_labels = None,
2505+ y_labels = None,
2506+ x_bounds = None,
2507+ y_bounds = None,
2508+ colors = None,
2509+ bar_borders=None):
2510+ #TODO: Fix docstring for vertical_bar_plot
2511+ '''
2512+ - Function to generate vertical Bar Plot Charts.
2513+
2514+ bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension,
2515+ x_labels, y_labels, x_bounds, y_bounds, colors):
2516+
2517+ - Parameters
2518+
2519+ name - Name of the desired output file, no need to input the .svg as it will be added at runtime;
2520+ data - The list, list of lists or dictionary holding the data to be plotted;
2521+ width, height - Dimensions of the output image;
2522+ background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient.
2523+ If left None, a gray to white gradient will be generated;
2524+ border - Distance in pixels of a square border into which the graphics will be drawn;
2525+ grid - Whether or not the gris is to be drawn;
2526+ rounded_corners - Whether or not the bars should have rounded corners;
2527+ three_dimension - Whether or not the bars should be drawn in pseudo 3D;
2528+ x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis;
2529+ x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted;
2530+ colors - List containing the colors expected for each of the bars.
2531+
2532+ - Example of use
2533+
2534+ data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2535+ CairoPlot.vertical_bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False)
2536+ '''
2537+
2538+ plot = VerticalBarPlot(name, data, width, height, background, border,
2539+ display_values, grid, rounded_corners, stack, three_dimension,
2540+ series_labels, x_labels, y_labels, x_bounds, y_bounds, colors)
2541+ plot.bar_borders = bar_borders or list()
2542+ plot.render()
2543+ plot.commit()
2544+
2545+def horizontal_bar_plot(name,
2546+ data,
2547+ width,
2548+ height,
2549+ background = "white light_gray",
2550+ border = 0,
2551+ display_values = False,
2552+ grid = False,
2553+ rounded_corners = False,
2554+ stack = False,
2555+ three_dimension = False,
2556+ series_labels = None,
2557+ x_labels = None,
2558+ y_labels = None,
2559+ x_bounds = None,
2560+ y_bounds = None,
2561+ colors = None):
2562+
2563+ #TODO: Fix docstring for horizontal_bar_plot
2564+ '''
2565+ - Function to generate Horizontal Bar Plot Charts.
2566+
2567+ bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension,
2568+ x_labels, y_labels, x_bounds, y_bounds, colors):
2569+
2570+ - Parameters
2571+
2572+ name - Name of the desired output file, no need to input the .svg as it will be added at runtime;
2573+ data - The list, list of lists or dictionary holding the data to be plotted;
2574+ width, height - Dimensions of the output image;
2575+ background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient.
2576+ If left None, a gray to white gradient will be generated;
2577+ border - Distance in pixels of a square border into which the graphics will be drawn;
2578+ grid - Whether or not the gris is to be drawn;
2579+ rounded_corners - Whether or not the bars should have rounded corners;
2580+ three_dimension - Whether or not the bars should be drawn in pseudo 3D;
2581+ x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis;
2582+ x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted;
2583+ colors - List containing the colors expected for each of the bars.
2584+
2585+ - Example of use
2586+
2587+ data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2588+ CairoPlot.bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False)
2589+ '''
2590+
2591+ plot = HorizontalBarPlot(name, data, width, height, background, border,
2592+ display_values, grid, rounded_corners, stack, three_dimension,
2593+ series_labels, x_labels, y_labels, x_bounds, y_bounds, colors)
2594+ plot.render()
2595+ plot.commit()
2596+
2597+def stream_chart(name,
2598+ data,
2599+ width,
2600+ height,
2601+ background = "white light_gray",
2602+ border = 0,
2603+ grid = False,
2604+ series_legend = None,
2605+ x_labels = None,
2606+ x_bounds = None,
2607+ y_bounds = None,
2608+ colors = None):
2609+
2610+ #TODO: Fix docstring for horizontal_bar_plot
2611+ plot = StreamChart(name, data, width, height, background, border,
2612+ grid, series_legend, x_labels, x_bounds, y_bounds, colors)
2613+ plot.render()
2614+ plot.commit()
2615+
2616+
2617+if __name__ == "__main__":
2618+ import tests
2619+ import seriestests
2620
2621=== added directory 'tools/development/query_sets'
2622=== added file 'tools/development/query_sets/timerange_always.txt'
2623--- tools/development/query_sets/timerange_always.txt 1970-01-01 00:00:00 +0000
2624+++ tools/development/query_sets/timerange_always.txt 2011-12-28 19:50:03 +0000
2625@@ -0,0 +1,9 @@
2626+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostRecentEvents
2627+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostRecentSubjects
2628+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostPopularSubjects
2629+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostPopularActor
2630+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostRecentActor
2631+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostRecentSubjectInterpretation
2632+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostPopularSubjectInterpretation
2633+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostRecentMimeType
2634+TimeRange.always(), [], StorageState.Any, 100, ResultType.MostPopularMimeType
2635
2636=== added file 'tools/development/query_sets/timerange_interval.txt'
2637--- tools/development/query_sets/timerange_interval.txt 1970-01-01 00:00:00 +0000
2638+++ tools/development/query_sets/timerange_interval.txt 2011-12-28 19:50:03 +0000
2639@@ -0,0 +1,9 @@
2640+(1, 60000), [], StorageState.Any, 6, ResultType.MostRecentEvents
2641+(1, 60000), [], StorageState.Any, 6, ResultType.MostRecentSubjects
2642+(1, 60000), [], StorageState.Any, 6, ResultType.MostPopularSubjects
2643+(1, 60000), [], StorageState.Any, 6, ResultType.MostPopularActor
2644+(1, 60000), [], StorageState.Any, 6, ResultType.MostRecentActor
2645+(1, 60000), [], StorageState.Any, 6, ResultType.MostRecentSubjectInterpretation
2646+(1, 60000), [], StorageState.Any, 6, ResultType.MostPopularSubjectInterpretation
2647+(1, 60000), [], StorageState.Any, 6, ResultType.MostRecentMimeType
2648+(1, 60000), [], StorageState.Any, 6, ResultType.MostPopularMimeType
2649
2650=== added file 'tools/development/query_timings.py'
2651--- tools/development/query_timings.py 1970-01-01 00:00:00 +0000
2652+++ tools/development/query_timings.py 2011-12-28 19:50:03 +0000
2653@@ -0,0 +1,240 @@
2654+#! /usr/bin/env python
2655+# -.- coding: utf-8 -.-
2656+
2657+# Zeitgeist
2658+#
2659+# Copyright © 2010 Markus Korn <thekorn@gmx.net>
2660+# Copyright © 2011 Collabora Ltd.
2661+# By Seif Lotfy <seif@lotfy.com>
2662+#
2663+# This program is free software: you can redistribute it and/or modify
2664+# it under the terms of the GNU Lesser General Public License as published by
2665+# the Free Software Foundation, either version 2.1 of the License, or
2666+# (at your option) any later version.
2667+#
2668+# This program is distributed in the hope that it will be useful,
2669+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2670+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2671+# GNU Lesser General Public License for more details.
2672+#
2673+# You should have received a copy of the GNU Lesser General Public License
2674+# along with this program. If not, see <http://www.gnu.org/licenses/>.
2675+
2676+# USAGE
2677+#
2678+# To run the benchmarks on a certain branch of zeitgeist make sure this
2679+# branch is in PYTHONPATH.
2680+# To benchmark and output overall to 'output.json' run
2681+# ./query_overalls.py -o output.json
2682+# If you already have data in the output file and want to merge both
2683+# data sets (build avg.) run
2684+# ./query_overalls.py -m -o output.json
2685+# To plot the data use the '--plot' argument, multible '--plot' arguments
2686+# will define multible series.
2687+# ./query_overalls.py --plot output.json --plot output1.json -o plot.svg
2688+# In short, a run always looks like:
2689+# PYTHONPATH=../trunk tools/development/query_overalls.py --name "lp:zeitgeist" -o trunk.json \
2690+# --queries tools/development/query_sets/timerange_always.txt
2691+# PYTHONPATH=. tools/development/query_overalls.py --name "lp:some-branch" -o somebranch.json
2692+# PYTHONPATH=. tools/development/query_overalls.py --plot somebranch.json --plot trunk.json -o benchmark.svg
2693+
2694+import os
2695+import random
2696+import time
2697+import json
2698+import sys
2699+import logging
2700+
2701+from optparse import OptionParser
2702+from logging import handlers
2703+from collections import defaultdict
2704+
2705+from zeitgeist.datamodel import TimeRange, StorageState, ResultType
2706+from zeitgeist.datamodel import Event
2707+import benchmark as engine
2708+
2709+from cairoplot import vertical_bar_plot
2710+
2711+
2712+class QueryPlanHandler(handlers.MemoryHandler):
2713+
2714+ @staticmethod
2715+ def get_plan(msg):
2716+ if "SELECT id FROM event_view" not in msg:
2717+ return None
2718+ msg = msg.splitlines()
2719+ if not "PLAN:" in msg:
2720+ return None
2721+ for plan in msg[msg.index("PLAN:")+1:]:
2722+ if "INDEX" not in plan and "PRIMARY KEY" not in plan:
2723+ return False
2724+ return True
2725+
2726+ def __init__(self):
2727+ handlers.MemoryHandler.__init__(self, 200000, logging.DEBUG)
2728+ self.uses_index = None
2729+
2730+ def emit(self, record):
2731+ x = self.get_plan(record.msg)
2732+ if x is not None:
2733+ if not x or self.uses_index is None:
2734+ self.uses_index = x
2735+ return handlers.MemoryHandler.emit(self, record)
2736+
2737+def get_reference_engine():
2738+ return engine
2739+
2740+def get_query_set(source):
2741+ for line in open(source):
2742+ yield line.strip()
2743+
2744+def get_cmdline():
2745+ parser = OptionParser()
2746+ parser.add_option("-o", dest="output", help="write output to FILE", metavar="FILE")
2747+ parser.add_option("--queries", dest="queryset", help="run all queries in FILE", metavar="FILE")
2748+ parser.add_option("--name", dest="name", help="name of the data series", metavar="NAME")
2749+ parser.add_option("-i", dest="isolated", action="store_true",
2750+ default=False, help="run each query isolated")
2751+ parser.add_option("-m", dest="merge", action="store_true",
2752+ default=False, help="if the datafile already contains data the new data gets merged")
2753+ parser.add_option("--plot", dest="plot_files", metavar="DATA_FILE",
2754+ action="append", type="str")
2755+ parser.add_option("--type", dest="type", help="type of plot")
2756+ (options, args) = parser.parse_args()
2757+ assert not args
2758+ return options
2759+
2760+def get_name(data, alternative_name):
2761+ try:
2762+ return data["__metadata__"]["name"]
2763+ except:
2764+ return alternative_name
2765+
2766+def get_data(dataset, query, key):
2767+ try:
2768+ return float(dataset[query][key])
2769+ except:
2770+ return 0.0
2771+
2772+def compare_queries(a, b):
2773+ result = cmp(a.strip().split()[-1], b.strip().split()[-1])
2774+ if result != 0:
2775+ return result
2776+ return cmp(a[0], b[0])
2777+
2778+def plot(output_filename, plot_type, *data_files):
2779+ raw_data = map(lambda x: json.load(open(x)), data_files)
2780+ series_labels = map(lambda x: get_name(x[1], data_files[x[0]]), enumerate(raw_data))
2781+ queries = sorted(
2782+ filter(lambda x: x != "__metadata__", set(sum([d.keys() for d in raw_data], []))),
2783+ cmp=compare_queries
2784+ )
2785+ data = []
2786+ max_value = 0
2787+ no_index = list()
2788+ style = plot_type
2789+ for n, query in enumerate(queries):
2790+ x = [get_data(d, query, style) for d in raw_data]
2791+ print x
2792+ y = max(x)
2793+ idx_border = [not d.get(query, {}).get("uses_index", True) for d in raw_data]
2794+ for i, b in enumerate(idx_border):
2795+ if b:
2796+ no_index.append((n, i))
2797+ if y > max_value:
2798+ max_value = y
2799+ data.append(x)
2800+ y_parts = max_value / float(4)
2801+ y_labels = ["%.5fs" %(i*y_parts) for i in range(5)]
2802+ vertical_bar_plot(
2803+ output_filename, data, len(queries)*400, 600,
2804+ x_labels=queries, y_labels=y_labels,
2805+ grid=True, series_labels=series_labels, bar_borders=no_index)
2806+
2807+
2808+if __name__ == "__main__":
2809+ options = get_cmdline()
2810+ if options.plot_files:
2811+ if options.type in ("marsh_time", "get_events_time", "find_ids_time", "find_events", "overall"):
2812+ assert options.output
2813+ plot(options.output, options.type, *options.plot_files)
2814+ else:
2815+ print "please specify plot type (marsh_time, get_events_time, find_ids_time, find_events, overall)"
2816+ else:
2817+ engine = get_reference_engine()
2818+ result = {}
2819+ if options.name:
2820+ result["__metadata__"] = {
2821+ "name": options.name,
2822+ }
2823+ if options.output and os.path.exists(options.output):
2824+ existing_data = json.load(open(options.output))
2825+ else:
2826+ existing_data = {}
2827+ logging.basicConfig(level=logging.DEBUG)
2828+ for query in get_query_set(options.queryset):
2829+ args = eval(query)
2830+ start_time = time.time()
2831+ logging.getLogger("").removeHandler(logging.getLogger("").handlers[0])
2832+ handler = QueryPlanHandler()
2833+ logging.getLogger("").addHandler(handler)
2834+ results = {}
2835+ for i in xrange (50):
2836+ t1 = time.time()
2837+ temp = engine.find_events(*args)
2838+ temp["overall"] = time.time() - t1
2839+ if len(results.keys()) == 0:
2840+ for key in results.keys():
2841+ temp[key] = temp[key]
2842+ results = temp
2843+ else:
2844+ for key in temp.keys():
2845+ if key != "events":
2846+ results[key] += temp[key]
2847+ print results[key]
2848+
2849+ for key in temp.keys():
2850+ if key != "events":
2851+ results[key] = results[key]/50
2852+
2853+ print (results.keys())
2854+
2855+ events = results["events"]
2856+ run_time = results["find_events"]
2857+ find_ids_time = results["overall"]
2858+ find_events_time = results["find_event_ids"]
2859+ get_events_time = results["get_events"]
2860+ marsh_time = results["marsh_events"]
2861+
2862+ print "===>", find_ids_time
2863+
2864+ if query in existing_data and options.merge:
2865+ print "=================================="
2866+ counter = existing_data[query].get("counter", 1)
2867+ old_time = existing_data[query]["overall"]
2868+ run_time = (old_time * counter + run_time)/(counter + 1)
2869+
2870+ result[query] = {
2871+ "overall": run_time,
2872+ "counter": counter + 1,
2873+ "find_ids_time": find_ids_time,
2874+ "get_events_time": get_events_time,
2875+ "find_events": find_events_time,
2876+ "marsh_time": marsh_time,
2877+ }
2878+ else:
2879+ result[query] = {
2880+ "overall": run_time,
2881+ "find_ids_time": find_ids_time,
2882+ "get_events_time": get_events_time,
2883+ "find_events": find_events_time,
2884+ "marsh_time": marsh_time,
2885+ }
2886+ if options.output:
2887+ f = open(options.output, "w")
2888+ else:
2889+ f = sys.stdout
2890+ try:
2891+ json.dump(result, f, indent=4)
2892+ finally:
2893+ f.close()
2894
2895=== added file 'tools/development/series.py'
2896--- tools/development/series.py 1970-01-01 00:00:00 +0000
2897+++ tools/development/series.py 2011-12-28 19:50:03 +0000
2898@@ -0,0 +1,1140 @@
2899+#!/usr/bin/env python
2900+# -*- coding: utf-8 -*-
2901+
2902+# Serie.py
2903+#
2904+# Copyright (c) 2008 Magnun Leno da Silva
2905+#
2906+# Author: Magnun Leno da Silva <magnun.leno@gmail.com>
2907+#
2908+# This program is free software; you can redistribute it and/or
2909+# modify it under the terms of the GNU Lesser General Public License
2910+# as published by the Free Software Foundation; either version 2 of
2911+# the License, or (at your option) any later version.
2912+#
2913+# This program is distributed in the hope that it will be useful,
2914+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2915+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2916+# GNU General Public License for more details.
2917+#
2918+# You should have received a copy of the GNU Lesser General Public
2919+# License along with this program; if not, write to the Free Software
2920+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
2921+# USA
2922+
2923+# Contributor: Rodrigo Moreiro Araujo <alf.rodrigo@gmail.com>
2924+
2925+#import cairoplot
2926+import doctest
2927+
2928+NUMTYPES = (int, float, long)
2929+LISTTYPES = (list, tuple)
2930+STRTYPES = (str, unicode)
2931+FILLING_TYPES = ['linear', 'solid', 'gradient']
2932+DEFAULT_COLOR_FILLING = 'solid'
2933+#TODO: Define default color list
2934+DEFAULT_COLOR_LIST = None
2935+
2936+class Data(object):
2937+ '''
2938+ Class that models the main data structure.
2939+ It can hold:
2940+ - a number type (int, float or long)
2941+ - a tuple, witch represents a point and can have 2 or 3 items (x,y,z)
2942+ - if a list is passed it will be converted to a tuple.
2943+
2944+ obs: In case a tuple is passed it will convert to tuple
2945+ '''
2946+ def __init__(self, data=None, name=None, parent=None):
2947+ '''
2948+ Starts main atributes from the Data class
2949+ @name - Name for each point;
2950+ @content - The real data, can be an int, float, long or tuple, which
2951+ represents a point (x,y) or (x,y,z);
2952+ @parent - A pointer that give the data access to it's parent.
2953+
2954+ Usage:
2955+ >>> d = Data(name='empty'); print d
2956+ empty: ()
2957+ >>> d = Data((1,1),'point a'); print d
2958+ point a: (1, 1)
2959+ >>> d = Data((1,2,3),'point b'); print d
2960+ point b: (1, 2, 3)
2961+ >>> d = Data([2,3],'point c'); print d
2962+ point c: (2, 3)
2963+ >>> d = Data(12, 'simple value'); print d
2964+ simple value: 12
2965+ '''
2966+ # Initial values
2967+ self.__content = None
2968+ self.__name = None
2969+
2970+ # Setting passed values
2971+ self.parent = parent
2972+ self.name = name
2973+ self.content = data
2974+
2975+ # Name property
2976+ @apply
2977+ def name():
2978+ doc = '''
2979+ Name is a read/write property that controls the input of name.
2980+ - If passed an invalid value it cleans the name with None
2981+
2982+ Usage:
2983+ >>> d = Data(13); d.name = 'name_test'; print d
2984+ name_test: 13
2985+ >>> d.name = 11; print d
2986+ 13
2987+ >>> d.name = 'other_name'; print d
2988+ other_name: 13
2989+ >>> d.name = None; print d
2990+ 13
2991+ >>> d.name = 'last_name'; print d
2992+ last_name: 13
2993+ >>> d.name = ''; print d
2994+ 13
2995+ '''
2996+ def fget(self):
2997+ '''
2998+ returns the name as a string
2999+ '''
3000+ return self.__name
3001+
3002+ def fset(self, name):
3003+ '''
3004+ Sets the name of the Data
3005+ '''
3006+ if type(name) in STRTYPES and len(name) > 0:
3007+ self.__name = name
3008+ else:
3009+ self.__name = None
3010+
3011+
3012+
3013+ return property(**locals())
3014+
3015+ # Content property
3016+ @apply
3017+ def content():
3018+ doc = '''
3019+ Content is a read/write property that validate the data passed
3020+ and return it.
3021+
3022+ Usage:
3023+ >>> d = Data(); d.content = 13; d.content
3024+ 13
3025+ >>> d = Data(); d.content = (1,2); d.content
3026+ (1, 2)
3027+ >>> d = Data(); d.content = (1,2,3); d.content
3028+ (1, 2, 3)
3029+ >>> d = Data(); d.content = [1,2,3]; d.content
3030+ (1, 2, 3)
3031+ >>> d = Data(); d.content = [1.5,.2,3.3]; d.content
3032+ (1.5, 0.20000000000000001, 3.2999999999999998)
3033+ '''
3034+ def fget(self):
3035+ '''
3036+ Return the content of Data
3037+ '''
3038+ return self.__content
3039+
3040+ def fset(self, data):
3041+ '''
3042+ Ensures that data is a valid tuple/list or a number (int, float
3043+ or long)
3044+ '''
3045+ # Type: None
3046+ if data is None:
3047+ self.__content = None
3048+ return
3049+
3050+ # Type: Int or Float
3051+ elif type(data) in NUMTYPES:
3052+ self.__content = data
3053+
3054+ # Type: List or Tuple
3055+ elif type(data) in LISTTYPES:
3056+ # Ensures the correct size
3057+ if len(data) not in (2, 3):
3058+ raise TypeError, "Data (as list/tuple) must have 2 or 3 items"
3059+ return
3060+
3061+ # Ensures that all items in list/tuple is a number
3062+ isnum = lambda x : type(x) not in NUMTYPES
3063+
3064+ if max(map(isnum, data)):
3065+ # An item in data isn't an int or a float
3066+ raise TypeError, "All content of data must be a number (int or float)"
3067+
3068+ # Convert the tuple to list
3069+ if type(data) is list:
3070+ data = tuple(data)
3071+
3072+ # Append a copy and sets the type
3073+ self.__content = data[:]
3074+
3075+ # Unknown type!
3076+ else:
3077+ self.__content = None
3078+ raise TypeError, "Data must be an int, float or a tuple with two or three items"
3079+ return
3080+
3081+ return property(**locals())
3082+
3083+
3084+ def clear(self):
3085+ '''
3086+ Clear the all Data (content, name and parent)
3087+ '''
3088+ self.content = None
3089+ self.name = None
3090+ self.parent = None
3091+
3092+ def copy(self):
3093+ '''
3094+ Returns a copy of the Data structure
3095+ '''
3096+ # The copy
3097+ new_data = Data()
3098+ if self.content is not None:
3099+ # If content is a point
3100+ if type(self.content) is tuple:
3101+ new_data.__content = self.content[:]
3102+
3103+ # If content is a number
3104+ else:
3105+ new_data.__content = self.content
3106+
3107+ # If it has a name
3108+ if self.name is not None:
3109+ new_data.__name = self.name
3110+
3111+ return new_data
3112+
3113+ def __str__(self):
3114+ '''
3115+ Return a string representation of the Data structure
3116+ '''
3117+ if self.name is None:
3118+ if self.content is None:
3119+ return ''
3120+ return str(self.content)
3121+ else:
3122+ if self.content is None:
3123+ return self.name+": ()"
3124+ return self.name+": "+str(self.content)
3125+
3126+ def __len__(self):
3127+ '''
3128+ Return the length of the Data.
3129+ - If it's a number return 1;
3130+ - If it's a list return it's length;
3131+ - If its None return 0.
3132+ '''
3133+ if self.content is None:
3134+ return 0
3135+ elif type(self.content) in NUMTYPES:
3136+ return 1
3137+ return len(self.content)
3138+
3139+
3140+
3141+
3142+class Group(object):
3143+ '''
3144+ Class that models a group of data. Every value (int, float, long, tuple
3145+ or list) passed is converted to a list of Data.
3146+ It can receive:
3147+ - A single number (int, float, long);
3148+ - A list of numbers;
3149+ - A tuple of numbers;
3150+ - An instance of Data;
3151+ - A list of Data;
3152+
3153+ Obs: If a tuple with 2 or 3 items is passed it is converted to a point.
3154+ If a tuple with only 1 item is passed it's converted to a number;
3155+ If a tuple with more than 2 items is passed it's converted to a
3156+ list of numbers
3157+ '''
3158+ def __init__(self, group=None, name=None, parent=None):
3159+ '''
3160+ Starts main atributes in Group instance.
3161+ @data_list - a list of data which forms the group;
3162+ @range - a range that represent the x axis of possible functions;
3163+ @name - name of the data group;
3164+ @parent - the Serie parent of this group.
3165+
3166+ Usage:
3167+ >>> g = Group(13, 'simple number'); print g
3168+ simple number ['13']
3169+ >>> g = Group((1,2), 'simple point'); print g
3170+ simple point ['(1, 2)']
3171+ >>> g = Group([1,2,3,4], 'list of numbers'); print g
3172+ list of numbers ['1', '2', '3', '4']
3173+ >>> g = Group((1,2,3,4),'int in tuple'); print g
3174+ int in tuple ['1', '2', '3', '4']
3175+ >>> g = Group([(1,2),(2,3),(3,4)], 'list of points'); print g
3176+ list of points ['(1, 2)', '(2, 3)', '(3, 4)']
3177+ >>> g = Group([[1,2,3],[1,2,3]], '2D coordinate lists'); print g
3178+ 2D coordinated lists ['(1, 1)', '(2, 2)', '(3, 3)']
3179+ >>> g = Group([[1,2],[1,2],[1,2]], '3D coordinate lists'); print g
3180+ 3D coordinated lists ['(1, 1, 1)', '(2, 2, 2)']
3181+ '''
3182+ # Initial values
3183+ self.__data_list = []
3184+ self.__range = []
3185+ self.__name = None
3186+
3187+
3188+ self.parent = parent
3189+ self.name = name
3190+ self.data_list = group
3191+
3192+ # Name property
3193+ @apply
3194+ def name():
3195+ doc = '''
3196+ Name is a read/write property that controls the input of name.
3197+ - If passed an invalid value it cleans the name with None
3198+
3199+ Usage:
3200+ >>> g = Group(13); g.name = 'name_test'; print g
3201+ name_test ['13']
3202+ >>> g.name = 11; print g
3203+ ['13']
3204+ >>> g.name = 'other_name'; print g
3205+ other_name ['13']
3206+ >>> g.name = None; print g
3207+ ['13']
3208+ >>> g.name = 'last_name'; print g
3209+ last_name ['13']
3210+ >>> g.name = ''; print g
3211+ ['13']
3212+ '''
3213+ def fget(self):
3214+ '''
3215+ Returns the name as a string
3216+ '''
3217+ return self.__name
3218+
3219+ def fset(self, name):
3220+ '''
3221+ Sets the name of the Group
3222+ '''
3223+ if type(name) in STRTYPES and len(name) > 0:
3224+ self.__name = name
3225+ else:
3226+ self.__name = None
3227+
3228+ return property(**locals())
3229+
3230+ # data_list property
3231+ @apply
3232+ def data_list():
3233+ doc = '''
3234+ The data_list is a read/write property that can be a list of
3235+ numbers, a list of points or a list of 2 or 3 coordinate lists. This
3236+ property uses mainly the self.add_data method.
3237+
3238+ Usage:
3239+ >>> g = Group(); g.data_list = 13; print g
3240+ ['13']
3241+ >>> g.data_list = (1,2); print g
3242+ ['(1, 2)']
3243+ >>> g.data_list = Data((1,2),'point a'); print g
3244+ ['point a: (1, 2)']
3245+ >>> g.data_list = [1,2,3]; print g
3246+ ['1', '2', '3']
3247+ >>> g.data_list = (1,2,3,4); print g
3248+ ['1', '2', '3', '4']
3249+ >>> g.data_list = [(1,2),(2,3),(3,4)]; print g
3250+ ['(1, 2)', '(2, 3)', '(3, 4)']
3251+ >>> g.data_list = [[1,2],[1,2]]; print g
3252+ ['(1, 1)', '(2, 2)']
3253+ >>> g.data_list = [[1,2],[1,2],[1,2]]; print g
3254+ ['(1, 1, 1)', '(2, 2, 2)']
3255+ >>> g.range = (10); g.data_list = lambda x:x**2; print g
3256+ ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)']
3257+ '''
3258+ def fget(self):
3259+ '''
3260+ Returns the value of data_list
3261+ '''
3262+ return self.__data_list
3263+
3264+ def fset(self, group):
3265+ '''
3266+ Ensures that group is valid.
3267+ '''
3268+ # None
3269+ if group is None:
3270+ self.__data_list = []
3271+
3272+ # Int/float/long or Instance of Data
3273+ elif type(group) in NUMTYPES or isinstance(group, Data):
3274+ # Clean data_list
3275+ self.__data_list = []
3276+ self.add_data(group)
3277+
3278+ # One point
3279+ elif type(group) is tuple and len(group) in (2,3):
3280+ self.__data_list = []
3281+ self.add_data(group)
3282+
3283+ # list of items
3284+ elif type(group) in LISTTYPES and type(group[0]) is not list:
3285+ # Clean data_list
3286+ self.__data_list = []
3287+ for item in group:
3288+ # try to append and catch an exception
3289+ self.add_data(item)
3290+
3291+ # function lambda
3292+ elif callable(group):
3293+ # Explicit is better than implicit
3294+ function = group
3295+ # Has range
3296+ if len(self.range) is not 0:
3297+ # Clean data_list
3298+ self.__data_list = []
3299+ # Generate values for the lambda function
3300+ for x in self.range:
3301+ #self.add_data((x,round(group(x),2)))
3302+ self.add_data((x,function(x)))
3303+
3304+ # Only have range in parent
3305+ elif self.parent is not None and len(self.parent.range) is not 0:
3306+ # Copy parent range
3307+ self.__range = self.parent.range[:]
3308+ # Clean data_list
3309+ self.__data_list = []
3310+ # Generate values for the lambda function
3311+ for x in self.range:
3312+ #self.add_data((x,round(group(x),2)))
3313+ self.add_data((x,function(x)))
3314+
3315+ # Don't have range anywhere
3316+ else:
3317+ # x_data don't exist
3318+ raise Exception, "Data argument is valid but to use function type please set x_range first"
3319+
3320+ # Coordinate Lists
3321+ elif type(group) in LISTTYPES and type(group[0]) is list:
3322+ # Clean data_list
3323+ self.__data_list = []
3324+ data = []
3325+ if len(group) == 3:
3326+ data = zip(group[0], group[1], group[2])
3327+ elif len(group) == 2:
3328+ data = zip(group[0], group[1])
3329+ else:
3330+ raise TypeError, "Only one list of coordinates was received."
3331+
3332+ for item in data:
3333+ self.add_data(item)
3334+
3335+ else:
3336+ raise TypeError, "Group type not supported"
3337+
3338+ return property(**locals())
3339+
3340+ @apply
3341+ def range():
3342+ doc = '''
3343+ The range is a read/write property that generates a range of values
3344+ for the x axis of the functions. When passed a tuple it almost works
3345+ like the built-in range funtion:
3346+ - 1 item, represent the end of the range started from 0;
3347+ - 2 items, represents the start and the end, respectively;
3348+ - 3 items, the last one represents the step;
3349+
3350+ When passed a list the range function understands as a valid range.
3351+
3352+ Usage:
3353+ >>> g = Group(); g.range = 10; print g.range
3354+ [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
3355+ >>> g = Group(); g.range = (5); print g.range
3356+ [0.0, 1.0, 2.0, 3.0, 4.0]
3357+ >>> g = Group(); g.range = (1,7); print g.range
3358+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
3359+ >>> g = Group(); g.range = (0,10,2); print g.range
3360+ [0.0, 2.0, 4.0, 6.0, 8.0]
3361+ >>>
3362+ >>> g = Group(); g.range = [0]; print g.range
3363+ [0.0]
3364+ >>> g = Group(); g.range = [0,10,20]; print g.range
3365+ [0.0, 10.0, 20.0]
3366+ '''
3367+ def fget(self):
3368+ '''
3369+ Returns the range
3370+ '''
3371+ return self.__range
3372+
3373+ def fset(self, x_range):
3374+ '''
3375+ Controls the input of a valid type and generate the range
3376+ '''
3377+ # if passed a simple number convert to tuple
3378+ if type(x_range) in NUMTYPES:
3379+ x_range = (x_range,)
3380+
3381+ # A list, just convert to float
3382+ if type(x_range) is list and len(x_range) > 0:
3383+ # Convert all to float
3384+ x_range = map(float, x_range)
3385+ # Prevents repeated values and convert back to list
3386+ self.__range = list(set(x_range[:]))
3387+ # Sort the list to ascending order
3388+ self.__range.sort()
3389+
3390+ # A tuple, must check the lengths and generate the values
3391+ elif type(x_range) is tuple and len(x_range) in (1,2,3):
3392+ # Convert all to float
3393+ x_range = map(float, x_range)
3394+
3395+ # Inital values
3396+ start = 0.0
3397+ step = 1.0
3398+ end = 0.0
3399+
3400+ # Only the end and it can't be less or iqual to 0
3401+ if len(x_range) is 1 and x_range > 0:
3402+ end = x_range[0]
3403+
3404+ # The start and the end but the start must be less then the end
3405+ elif len(x_range) is 2 and x_range[0] < x_range[1]:
3406+ start = x_range[0]
3407+ end = x_range[1]
3408+
3409+ # All 3, but the start must be less then the end
3410+ elif x_range[0] <= x_range[1]:
3411+ start = x_range[0]
3412+ end = x_range[1]
3413+ step = x_range[2]
3414+
3415+ # Starts the range
3416+ self.__range = []
3417+ # Generate the range
3418+ # Can't use the range function because it doesn't support float values
3419+ while start < end:
3420+ self.__range.append(start)
3421+ start += step
3422+
3423+ # Incorrect type
3424+ else:
3425+ raise Exception, "x_range must be a list with one or more items or a tuple with 2 or 3 items"
3426+
3427+ return property(**locals())
3428+
3429+ def add_data(self, data, name=None):
3430+ '''
3431+ Append a new data to the data_list.
3432+ - If data is an instance of Data, append it
3433+ - If it's an int, float, tuple or list create an instance of Data and append it
3434+
3435+ Usage:
3436+ >>> g = Group()
3437+ >>> g.add_data(12); print g
3438+ ['12']
3439+ >>> g.add_data(7,'other'); print g
3440+ ['12', 'other: 7']
3441+ >>>
3442+ >>> g = Group()
3443+ >>> g.add_data((1,1),'a'); print g
3444+ ['a: (1, 1)']
3445+ >>> g.add_data((2,2),'b'); print g
3446+ ['a: (1, 1)', 'b: (2, 2)']
3447+ >>>
3448+ >>> g.add_data(Data((1,2),'c')); print g
3449+ ['a: (1, 1)', 'b: (2, 2)', 'c: (1, 2)']
3450+ '''
3451+ if not isinstance(data, Data):
3452+ # Try to convert
3453+ data = Data(data,name,self)
3454+
3455+ if data.content is not None:
3456+ self.__data_list.append(data.copy())
3457+ self.__data_list[-1].parent = self
3458+
3459+
3460+ def to_list(self):
3461+ '''
3462+ Returns the group as a list of numbers (int, float or long) or a
3463+ list of tuples (points 2D or 3D).
3464+
3465+ Usage:
3466+ >>> g = Group([1,2,3,4],'g1'); g.to_list()
3467+ [1, 2, 3, 4]
3468+ >>> g = Group([(1,2),(2,3),(3,4)],'g2'); g.to_list()
3469+ [(1, 2), (2, 3), (3, 4)]
3470+ >>> g = Group([(1,2,3),(3,4,5)],'g2'); g.to_list()
3471+ [(1, 2, 3), (3, 4, 5)]
3472+ '''
3473+ return [data.content for data in self]
3474+
3475+ def copy(self):
3476+ '''
3477+ Returns a copy of this group
3478+ '''
3479+ new_group = Group()
3480+ new_group.__name = self.__name
3481+ if self.__range is not None:
3482+ new_group.__range = self.__range[:]
3483+ for data in self:
3484+ new_group.add_data(data.copy())
3485+ return new_group
3486+
3487+ def get_names(self):
3488+ '''
3489+ Return a list with the names of all data in this group
3490+ '''
3491+ names = []
3492+ for data in self:
3493+ if data.name is None:
3494+ names.append('Data '+str(data.index()+1))
3495+ else:
3496+ names.append(data.name)
3497+ return names
3498+
3499+
3500+ def __str__ (self):
3501+ '''
3502+ Returns a string representing the Group
3503+ '''
3504+ ret = ""
3505+ if self.name is not None:
3506+ ret += self.name + " "
3507+ if len(self) > 0:
3508+ list_str = [str(item) for item in self]
3509+ ret += str(list_str)
3510+ else:
3511+ ret += "[]"
3512+ return ret
3513+
3514+ def __getitem__(self, key):
3515+ '''
3516+ Makes a Group iterable, based in the data_list property
3517+ '''
3518+ return self.data_list[key]
3519+
3520+ def __len__(self):
3521+ '''
3522+ Returns the length of the Group, based in the data_list property
3523+ '''
3524+ return len(self.data_list)
3525+
3526+
3527+class Colors(object):
3528+ '''
3529+ Class that models the colors its labels (names) and its properties, RGB
3530+ and filling type.
3531+
3532+ It can receive:
3533+ - A list where each item is a list with 3 or 4 items. The
3534+ first 3 items represent the RGB values and the last argument
3535+ defines the filling type. The list will be converted to a dict
3536+ and each color will receve a name based in its position in the
3537+ list.
3538+ - A dictionary where each key will be the color name and its item
3539+ can be a list with 3 or 4 items. The first 3 items represent
3540+ the RGB colors and the last argument defines the filling type.
3541+ '''
3542+ def __init__(self, color_list=None):
3543+ '''
3544+ Start the color_list property
3545+ @ color_list - the list or dict contaning the colors properties.
3546+ '''
3547+ self.__color_list = None
3548+
3549+ self.color_list = color_list
3550+
3551+ @apply
3552+ def color_list():
3553+ doc = '''
3554+ >>> c = Colors([[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']])
3555+ >>> print c.color_list
3556+ {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']}
3557+ >>> c.color_list = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')]
3558+ >>> print c.color_list
3559+ {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']}
3560+ >>> c.color_list = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)}
3561+ >>> print c.color_list
3562+ {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']}
3563+ '''
3564+ def fget(self):
3565+ '''
3566+ Return the color list
3567+ '''
3568+ return self.__color_list
3569+
3570+ def fset(self, color_list):
3571+ '''
3572+ Format the color list to a dictionary
3573+ '''
3574+ if color_list is None:
3575+ self.__color_list = None
3576+ return
3577+
3578+ if type(color_list) in LISTTYPES and type(color_list[0]) in LISTTYPES:
3579+ old_color_list = color_list[:]
3580+ color_list = {}
3581+ for index, color in enumerate(old_color_list):
3582+ if len(color) is 3 and max(map(type, color)) in NUMTYPES:
3583+ color_list['Color '+str(index+1)] = list(color)+[DEFAULT_COLOR_FILLING]
3584+ elif len(color) is 4 and max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES:
3585+ color_list['Color '+str(index+1)] = list(color)
3586+ else:
3587+ raise TypeError, "Unsuported color format"
3588+ elif type(color_list) is not dict:
3589+ raise TypeError, "Unsuported color format"
3590+
3591+ for name, color in color_list.items():
3592+ if len(color) is 3:
3593+ if max(map(type, color)) in NUMTYPES:
3594+ color_list[name] = list(color)+[DEFAULT_COLOR_FILLING]
3595+ else:
3596+ raise TypeError, "Unsuported color format"
3597+ elif len(color) is 4:
3598+ if max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES:
3599+ color_list[name] = list(color)
3600+ else:
3601+ raise TypeError, "Unsuported color format"
3602+ self.__color_list = color_list.copy()
3603+
3604+ return property(**locals())
3605+
3606+
3607+class Series(object):
3608+ '''
3609+ Class that models a Series (group of groups). Every value (int, float,
3610+ long, tuple or list) passed is converted to a list of Group or Data.
3611+ It can receive:
3612+ - a single number or point, will be converted to a Group of one Data;
3613+ - a list of numbers, will be converted to a group of numbers;
3614+ - a list of tuples, will converted to a single Group of points;
3615+ - a list of lists of numbers, each 'sublist' will be converted to a
3616+ group of numbers;
3617+ - a list of lists of tuples, each 'sublist' will be converted to a
3618+ group of points;
3619+ - a list of lists of lists, the content of the 'sublist' will be
3620+ processed as coordinated lists and the result will be converted to
3621+ a group of points;
3622+ - a Dictionary where each item can be the same of the list: number,
3623+ point, list of numbers, list of points or list of lists (coordinated
3624+ lists);
3625+ - an instance of Data;
3626+ - an instance of group.
3627+ '''
3628+ def __init__(self, series=None, name=None, property=[], colors=None):
3629+ '''
3630+ Starts main atributes in Group instance.
3631+ @series - a list, dict of data of which the series is composed;
3632+ @name - name of the series;
3633+ @property - a list/dict of properties to be used in the plots of
3634+ this Series
3635+
3636+ Usage:
3637+ >>> print Series([1,2,3,4])
3638+ ["Group 1 ['1', '2', '3', '4']"]
3639+ >>> print Series([[1,2,3],[4,5,6]])
3640+ ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"]
3641+ >>> print Series((1,2))
3642+ ["Group 1 ['(1, 2)']"]
3643+ >>> print Series([(1,2),(2,3)])
3644+ ["Group 1 ['(1, 2)', '(2, 3)']"]
3645+ >>> print Series([[(1,2),(2,3)],[(4,5),(5,6)]])
3646+ ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"]
3647+ >>> print Series([[[1,2,3],[1,2,3],[1,2,3]]])
3648+ ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"]
3649+ >>> print Series({'g1':[1,2,3], 'g2':[4,5,6]})
3650+ ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"]
3651+ >>> print Series({'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]})
3652+ ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"]
3653+ >>> print Series({'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]})
3654+ ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"]
3655+ >>> print Series(Data(1,'d1'))
3656+ ["Group 1 ['d1: 1']"]
3657+ >>> print Series(Group([(1,2),(2,3)],'g1'))
3658+ ["g1 ['(1, 2)', '(2, 3)']"]
3659+ '''
3660+ # Intial values
3661+ self.__group_list = []
3662+ self.__name = None
3663+ self.__range = None
3664+
3665+ # TODO: Implement colors with filling
3666+ self.__colors = None
3667+
3668+ self.name = name
3669+ self.group_list = series
3670+ self.colors = colors
3671+
3672+ # Name property
3673+ @apply
3674+ def name():
3675+ doc = '''
3676+ Name is a read/write property that controls the input of name.
3677+ - If passed an invalid value it cleans the name with None
3678+
3679+ Usage:
3680+ >>> s = Series(13); s.name = 'name_test'; print s
3681+ name_test ["Group 1 ['13']"]
3682+ >>> s.name = 11; print s
3683+ ["Group 1 ['13']"]
3684+ >>> s.name = 'other_name'; print s
3685+ other_name ["Group 1 ['13']"]
3686+ >>> s.name = None; print s
3687+ ["Group 1 ['13']"]
3688+ >>> s.name = 'last_name'; print s
3689+ last_name ["Group 1 ['13']"]
3690+ >>> s.name = ''; print s
3691+ ["Group 1 ['13']"]
3692+ '''
3693+ def fget(self):
3694+ '''
3695+ Returns the name as a string
3696+ '''
3697+ return self.__name
3698+
3699+ def fset(self, name):
3700+ '''
3701+ Sets the name of the Group
3702+ '''
3703+ if type(name) in STRTYPES and len(name) > 0:
3704+ self.__name = name
3705+ else:
3706+ self.__name = None
3707+
3708+ return property(**locals())
3709+
3710+
3711+
3712+ # Colors property
3713+ @apply
3714+ def colors():
3715+ doc = '''
3716+ >>> s = Series()
3717+ >>> s.colors = [[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']]
3718+ >>> print s.colors
3719+ {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']}
3720+ >>> s.colors = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')]
3721+ >>> print s.colors
3722+ {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']}
3723+ >>> s.colors = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)}
3724+ >>> print s.colors
3725+ {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']}
3726+ '''
3727+ def fget(self):
3728+ '''
3729+ Return the color list
3730+ '''
3731+ return self.__colors.color_list
3732+
3733+ def fset(self, colors):
3734+ '''
3735+ Format the color list to a dictionary
3736+ '''
3737+ self.__colors = Colors(colors)
3738+
3739+ return property(**locals())
3740+
3741+ @apply
3742+ def range():
3743+ doc = '''
3744+ The range is a read/write property that generates a range of values
3745+ for the x axis of the functions. When passed a tuple it almost works
3746+ like the built-in range funtion:
3747+ - 1 item, represent the end of the range started from 0;
3748+ - 2 items, represents the start and the end, respectively;
3749+ - 3 items, the last one represents the step;
3750+
3751+ When passed a list the range function understands as a valid range.
3752+
3753+ Usage:
3754+ >>> s = Series(); s.range = 10; print s.range
3755+ [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
3756+ >>> s = Series(); s.range = (5); print s.range
3757+ [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
3758+ >>> s = Series(); s.range = (1,7); print s.range
3759+ [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]
3760+ >>> s = Series(); s.range = (0,10,2); print s.range
3761+ [0.0, 2.0, 4.0, 6.0, 8.0, 10.0]
3762+ >>>
3763+ >>> s = Series(); s.range = [0]; print s.range
3764+ [0.0]
3765+ >>> s = Series(); s.range = [0,10,20]; print s.range
3766+ [0.0, 10.0, 20.0]
3767+ '''
3768+ def fget(self):
3769+ '''
3770+ Returns the range
3771+ '''
3772+ return self.__range
3773+
3774+ def fset(self, x_range):
3775+ '''
3776+ Controls the input of a valid type and generate the range
3777+ '''
3778+ # if passed a simple number convert to tuple
3779+ if type(x_range) in NUMTYPES:
3780+ x_range = (x_range,)
3781+
3782+ # A list, just convert to float
3783+ if type(x_range) is list and len(x_range) > 0:
3784+ # Convert all to float
3785+ x_range = map(float, x_range)
3786+ # Prevents repeated values and convert back to list
3787+ self.__range = list(set(x_range[:]))
3788+ # Sort the list to ascending order
3789+ self.__range.sort()
3790+
3791+ # A tuple, must check the lengths and generate the values
3792+ elif type(x_range) is tuple and len(x_range) in (1,2,3):
3793+ # Convert all to float
3794+ x_range = map(float, x_range)
3795+
3796+ # Inital values
3797+ start = 0.0
3798+ step = 1.0
3799+ end = 0.0
3800+
3801+ # Only the end and it can't be less or iqual to 0
3802+ if len(x_range) is 1 and x_range > 0:
3803+ end = x_range[0]
3804+
3805+ # The start and the end but the start must be lesser then the end
3806+ elif len(x_range) is 2 and x_range[0] < x_range[1]:
3807+ start = x_range[0]
3808+ end = x_range[1]
3809+
3810+ # All 3, but the start must be lesser then the end
3811+ elif x_range[0] < x_range[1]:
3812+ start = x_range[0]
3813+ end = x_range[1]
3814+ step = x_range[2]
3815+
3816+ # Starts the range
3817+ self.__range = []
3818+ # Generate the range
3819+ # Cnat use the range function becouse it don't suport float values
3820+ while start <= end:
3821+ self.__range.append(start)
3822+ start += step
3823+
3824+ # Incorrect type
3825+ else:
3826+ raise Exception, "x_range must be a list with one or more item or a tuple with 2 or 3 items"
3827+
3828+ return property(**locals())
3829+
3830+ @apply
3831+ def group_list():
3832+ doc = '''
3833+ The group_list is a read/write property used to pre-process the list
3834+ of Groups.
3835+ It can be:
3836+ - a single number, point or lambda, will be converted to a single
3837+ Group of one Data;
3838+ - a list of numbers, will be converted to a group of numbers;
3839+ - a list of tuples, will converted to a single Group of points;
3840+ - a list of lists of numbers, each 'sublist' will be converted to
3841+ a group of numbers;
3842+ - a list of lists of tuples, each 'sublist' will be converted to a
3843+ group of points;
3844+ - a list of lists of lists, the content of the 'sublist' will be
3845+ processed as coordinated lists and the result will be converted
3846+ to a group of points;
3847+ - a list of lambdas, each lambda represents a Group;
3848+ - a Dictionary where each item can be the same of the list: number,
3849+ point, list of numbers, list of points, list of lists
3850+ (coordinated lists) or lambdas
3851+ - an instance of Data;
3852+ - an instance of group.
3853+
3854+ Usage:
3855+ >>> s = Series()
3856+ >>> s.group_list = [1,2,3,4]; print s
3857+ ["Group 1 ['1', '2', '3', '4']"]
3858+ >>> s.group_list = [[1,2,3],[4,5,6]]; print s
3859+ ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"]
3860+ >>> s.group_list = (1,2); print s
3861+ ["Group 1 ['(1, 2)']"]
3862+ >>> s.group_list = [(1,2),(2,3)]; print s
3863+ ["Group 1 ['(1, 2)', '(2, 3)']"]
3864+ >>> s.group_list = [[(1,2),(2,3)],[(4,5),(5,6)]]; print s
3865+ ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"]
3866+ >>> s.group_list = [[[1,2,3],[1,2,3],[1,2,3]]]; print s
3867+ ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"]
3868+ >>> s.group_list = [(0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)]; print s
3869+ ["Group 1 ['(0.5, 5.5)']", "Group 2 ['(0, 4)', '(6, 8)']", "Group 3 ['(5.5, 7)']", "Group 4 ['(7, 9)']"]
3870+ >>> s.group_list = {'g1':[1,2,3], 'g2':[4,5,6]}; print s
3871+ ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"]
3872+ >>> s.group_list = {'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}; print s
3873+ ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"]
3874+ >>> s.group_list = {'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}; print s
3875+ ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"]
3876+ >>> s.range = 10
3877+ >>> s.group_list = lambda x:x*2
3878+ >>> s.group_list = [lambda x:x*2, lambda x:x**2, lambda x:x**3]; print s
3879+ ["Group 1 ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "Group 2 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']", "Group 3 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']"]
3880+ >>> s.group_list = {'linear':lambda x:x*2, 'square':lambda x:x**2, 'cubic':lambda x:x**3}; print s
3881+ ["cubic ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']", "linear ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "square ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']"]
3882+ >>> s.group_list = Data(1,'d1'); print s
3883+ ["Group 1 ['d1: 1']"]
3884+ >>> s.group_list = Group([(1,2),(2,3)],'g1'); print s
3885+ ["g1 ['(1, 2)', '(2, 3)']"]
3886+ '''
3887+ def fget(self):
3888+ '''
3889+ Return the group list.
3890+ '''
3891+ return self.__group_list
3892+
3893+ def fset(self, series):
3894+ '''
3895+ Controls the input of a valid group list.
3896+ '''
3897+ #TODO: Add support to the following strem of data: [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)]
3898+
3899+ # Type: None
3900+ if series is None:
3901+ self.__group_list = []
3902+
3903+ # List or Tuple
3904+ elif type(series) in LISTTYPES:
3905+ self.__group_list = []
3906+
3907+ is_function = lambda x: callable(x)
3908+ # Groups
3909+ if list in map(type, series) or max(map(is_function, series)):
3910+ for group in series:
3911+ self.add_group(group)
3912+
3913+ # single group
3914+ else:
3915+ self.add_group(series)
3916+
3917+ #old code
3918+ ## List of numbers
3919+ #if type(series[0]) in NUMTYPES or type(series[0]) is tuple:
3920+ # print series
3921+ # self.add_group(series)
3922+ #
3923+ ## List of anything else
3924+ #else:
3925+ # for group in series:
3926+ # self.add_group(group)
3927+
3928+ # Dict representing series of groups
3929+ elif type(series) is dict:
3930+ self.__group_list = []
3931+ names = series.keys()
3932+ names.sort()
3933+ for name in names:
3934+ self.add_group(Group(series[name],name,self))
3935+
3936+ # A single lambda
3937+ elif callable(series):
3938+ self.__group_list = []
3939+ self.add_group(series)
3940+
3941+ # Int/float, instance of Group or Data
3942+ elif type(series) in NUMTYPES or isinstance(series, Group) or isinstance(series, Data):
3943+ self.__group_list = []
3944+ self.add_group(series)
3945+
3946+ # Default
3947+ else:
3948+ raise TypeError, "Serie type not supported"
3949+
3950+ return property(**locals())
3951+
3952+ def add_group(self, group, name=None):
3953+ '''
3954+ Append a new group in group_list
3955+ '''
3956+ if not isinstance(group, Group):
3957+ #Try to convert
3958+ group = Group(group, name, self)
3959+
3960+ if len(group.data_list) is not 0:
3961+ # Auto naming groups
3962+ if group.name is None:
3963+ group.name = "Group "+str(len(self.__group_list)+1)
3964+
3965+ self.__group_list.append(group)
3966+ self.__group_list[-1].parent = self
3967+
3968+ def copy(self):
3969+ '''
3970+ Returns a copy of the Series
3971+ '''
3972+ new_series = Series()
3973+ new_series.__name = self.__name
3974+ if self.__range is not None:
3975+ new_series.__range = self.__range[:]
3976+ #Add color property in the copy method
3977+ #self.__colors = None
3978+
3979+ for group in self:
3980+ new_series.add_group(group.copy())
3981+
3982+ return new_series
3983+
3984+ def get_names(self):
3985+ '''
3986+ Returns a list of the names of all groups in the Serie
3987+ '''
3988+ names = []
3989+ for group in self:
3990+ if group.name is None:
3991+ names.append('Group '+str(group.index()+1))
3992+ else:
3993+ names.append(group.name)
3994+
3995+ return names
3996+
3997+ def to_list(self):
3998+ '''
3999+ Returns a list with the content of all groups and data
4000+ '''
4001+ big_list = []
4002+ for group in self:
4003+ for data in group:
4004+ if type(data.content) in NUMTYPES:
4005+ big_list.append(data.content)
4006+ else:
4007+ big_list = big_list + list(data.content)
4008+ return big_list
4009+
4010+ def __getitem__(self, key):
4011+ '''
4012+ Makes the Series iterable, based in the group_list property
4013+ '''
4014+ return self.__group_list[key]
4015+
4016+ def __str__(self):
4017+ '''
4018+ Returns a string that represents the Series
4019+ '''
4020+ ret = ""
4021+ if self.name is not None:
4022+ ret += self.name + " "
4023+ if len(self) > 0:
4024+ list_str = [str(item) for item in self]
4025+ ret += str(list_str)
4026+ else:
4027+ ret += "[]"
4028+ return ret
4029+
4030+ def __len__(self):
4031+ '''
4032+ Returns the length of the Series, based in the group_lsit property
4033+ '''
4034+ return len(self.group_list)
4035+
4036+
4037+if __name__ == '__main__':
4038+ doctest.testmod()

Subscribers

People subscribed via source and target branches