Adding Pychart due to bug in recent pychart versions
[odoo/odoo.git] / bin / pychart / bar_plot.py
1 #
2 # Copyright (C) 2000-2005 by Yasushi Saito (yasushi.saito@gmail.com)
3
4 # Jockey is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License as published by the
6 # Free Software Foundation; either version 2, or (at your option) any
7 # later version.
8 #
9 # Jockey is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 # for more details.
13 #
14 import line_style
15 import fill_style
16 import pychart_util
17 import chart_object
18 import legend
19 import error_bar
20 import bar_plot_doc
21 import theme
22 from types import *
23 from pychart_types import *
24
25 fill_styles = None
26
27 _keys = {
28     "direction" : (StringType, "vertical",
29                    """The direction the growth of the bars. The value is either 'horizontal'
30                    or 'vertical'."""),
31     "data" : (AnyType, None, pychart_util.data_desc),
32     "data_label_offset": (CoordType, (0, 5),
33                           "The location of data labels relative to the sample point. See also attribute data_label_format."),
34     
35     "data_label_format": (FormatType, None, """The
36                           format string for the label displayed besides each
37                           bar.  It can be a `printf' style format
38                           string, or a two-parameter function that
39                           takes (x,y) values and returns a string. """
40                           + pychart_util.string_desc),
41     
42     "label": (StringType, "???", pychart_util.label_desc), 
43     "bcol" : (IntType, 0,
44               """Specifies the column from which base values (i.e., X values when attribute "direction" is "vertical", Y values otherwise) are extracted.
45 The
46               combination of "data", "bcol", and "hcol" attributes defines
47               the set of boxes drawn by this chart. See the
48               below example:
49               
50 @example
51               d = [[5,10], [7,22], [8,25]]
52               p = bar_plot.T(data = d, bcol = 1, hcol = 2)
53 @end example
54
55               Here, three bars will be drawn. The X values of the bars
56               will be 5, 7, and 8. The Y values of the bars will be
57               10, 22, and 25, respectively. (In practice, because
58               the values of bcol and hcol defaults to 1 and 2, you can
59               write the above example just as "p = bar_plot.T(data = d)".
60               """),
61     "hcol": (IntType, 1,
62              """The column from which the height of each bar is extracted.
63              See also the description of the 'bcol' attribute."""),
64     "line_style": (line_style.T, line_style.default,
65                    "The style of the outer frame of each box."),
66     "fill_style": (fill_style.T, lambda: fill_styles.next(),
67                    "Defines the fill style of each box.",
68                    "The style is picked from standard styles round-robin."),
69     "legend_line_style": (line_style.T, None,
70                           """The line style used to draw a legend entry. Usually, the value is None, meaning that the value of "line_style" attribute is used."""),
71     "legend_fill_style": (fill_style.T, None,
72                    """The fill style used to draw a legend entry. Usually, the value is None, meaning that the value of "fill_style" attribute is used."""),
73                           
74     "cluster": (TupleType, (0, 1), """This attribute is used to
75     cluster multiple bar plots side by side in a single chart.
76     The value should be a tuple of two integers. The second value should be equal to the total number of bar plots in the chart. The first value should be the relative position of this chart; 0 places this chart the leftmost, and N-1
77     (where N is the 2nd value of this attribute) places this chart the rightmost. Consider the below example:
78
79 @example
80     a = area.T(...)
81     p1 = bar_plot.T(data = [[1,20][2,30]], cluster=(0,2))
82     p2 = bar_plot.T(data = [[1,25],[2,10]], cluster=(1,2))
83     a.add_plot(p1, p2)
84     a.draw()
85 @end example
86
87     In this example, one group of bars will be drawn side-by-side at
88     position x=1, one with height 20, the other with height 25. The
89     other two bars will be drawn side by side at position x=2, one
90     with height 30 and the other with height 10.
91     """),
92     "width": (UnitType, 5, """Width of each box. 
93 @cindex width, bar chart
94 @cindex size, bar chart
95 """),
96     "cluster_sep": (UnitType, 0, """The separation between
97     clustered boxes."""),
98     "stack_on": (AnyType, None,
99                  "The value must be either None or bar_plot.T. If not None, bars of this plot are stacked on top of another bar plot."),
100     "error_minus_col": (IntType, -1,
101                   """Specifies the column from which the depth of the errorbar is extracted.  This attribute is meaningful only when
102                   error_bar != None.
103                   """),
104     "qerror_minus_col":  (IntType, -1,
105                   """The depth of the "quartile" errorbar is extracted from 
106                   this column in data. This attribute is meaningful only
107                   when error_bar != None. """),
108     "error_plus_col": (IntType, -1,
109                   """The depth of the errorbar is extracted from 
110                   this column in data. This attribute is meaningful only
111                   when error_bar != None."""),
112     "qerror_plus_col":  (IntType, -1, 
113                   """The depth of the "quartile" errorbar is extracted from 
114                   this column in data. This attribute is meaningful only
115                   when error_bar != None."""),
116     "error_bar": (error_bar.T, None,
117                   "Specifies the style of the error bar. <<error_bar>>"),
118     "_abs_data" : (ListType, None,
119                    "Used only internally."),
120     }
121
122 def find_bar_plot(ar, nth):
123     "Find the NTH barplot of the cluster in area AR."
124     for plot in ar.plots:
125         if isinstance(plot, T) and plot.cluster[0] == nth:
126             return plot
127     raise Exception, "The %dth bar plot in the cluster not found." % nth   
128
129 class T(chart_object.T):
130     __doc__ = bar_plot_doc.doc
131     keys = _keys
132     def check_integrity(self):
133         self.type_check()
134         self.compute_abs_data()
135     def compute_abs_data(self):
136         if self._abs_data != None:
137             return
138         
139         if self.stack_on == None:
140             self._abs_data = self.data
141         else:
142             n = []
143             for pair in self.data:
144                 self.stack_on.compute_abs_data()
145                 newpair = list(pair[:])
146                 newpair[self.hcol] = self.stack_on.get_value(newpair[self.bcol]) + pair[self.hcol]
147                 n.append(newpair)
148             self._abs_data = n
149             
150 ##AUTOMATICALLY GENERATED
151
152 ##END AUTOMATICALLY GENERATED
153     def get_value(self, bval):
154         for pair in self._abs_data:
155             if pair[self.bcol] == bval:
156                 return pair[self.hcol]
157         raise ValueError, str(bval) + ": can't find the xval"
158
159     def get_data_range(self, which):
160         if self.direction == 'vertical':
161             if which == 'X':
162                 return pychart_util.get_data_range(self._abs_data, self.bcol)
163             else:
164                 return pychart_util.get_data_range(self._abs_data, self.hcol)
165         else:
166             assert self.direction == 'horizontal'
167             if which == 'Y':
168                 return pychart_util.get_data_range(self._abs_data, self.bcol)
169             else:
170                 return pychart_util.get_data_range(self._abs_data, self.hcol)
171
172     def get_bar_width(self, ar, nth):
173         off = 0
174         for i in range(0, nth):
175             plot = find_bar_plot(ar, i)
176             off += plot.width + plot.cluster_sep
177         return off
178     
179     def draw_vertical(self, ar, can):
180         for pair in self.data:
181             xval = pair[self.bcol]
182             yval = pychart_util.get_sample_val(pair, self.hcol)
183             
184             if None in (xval, yval): continue
185
186             ybot = 0
187             if self.stack_on:
188                 ybot = self.stack_on.get_value(xval)
189                 yval += ybot
190
191             totalWidth = self.get_bar_width(ar, self.cluster[1])
192             firstX = ar.x_pos(xval) - totalWidth/2.0
193             thisX = firstX + self.get_bar_width(ar, self.cluster[0])
194
195             can.rectangle(self.line_style, self.fill_style,
196                              thisX, ar.y_pos(ybot), thisX+self.width, 
197                              ar.y_pos(yval))
198
199             if self.error_bar:
200                 plus = pair[self.error_minus_col or self.error_plus_col]
201                 minus = pair[self.error_plus_col or self.error_minus_col]
202                 qplus = 0
203                 qminus = 0
204                 if self.qerror_minus_col or self.qerror_plus_col:
205                     qplus = pair[self.qerror_minus_col or self.qerror_plus_col]
206                     qminus = pair[self.qerror_plus_col or self.qerror_minus_col]
207                 if None not in (plus, minus, qplus, qminus): 
208                     self.error_bar.draw(can, (thisX+self.width/2.0, ar.y_pos(yval)),
209                                         ar.y_pos(yval - minus),
210                                         ar.y_pos(yval + plus),
211                                         ar.y_pos(yval - qminus),
212                                         ar.y_pos(yval + qplus))
213                     
214             if self.data_label_format:
215                 can.show(thisX + self.width/2.0 + self.data_label_offset[0],
216                             ar.y_pos(yval) + self.data_label_offset[1],
217                             "/hC" + pychart_util.apply_format(self.data_label_format, (pair[self.bcol], pair[self.hcol]), 1))
218             
219     def draw_horizontal(self, ar, can):
220         for pair in self.data:
221             yval = pair[self.bcol]
222             xval = pychart_util.get_sample_val(pair, self.hcol)
223
224             if None in (xval, yval): continue
225
226             xbot = 0
227             if self.stack_on:
228                 xbot = self.stack_on.get_value(yval)
229                 xval += xbot
230             totalWidth = self.get_bar_width(ar, self.cluster[1])
231             firstY = ar.y_pos(yval) - totalWidth/2.0
232             thisY = firstY + self.get_bar_width(ar, self.cluster[0])
233             
234             can.rectangle(self.line_style, self.fill_style,
235                           ar.x_pos(xbot), thisY,
236                           ar.x_pos(xval), thisY+self.width)
237
238             if self.data_label_format:
239                 can.show(ar.x_pos(xval) + self.data_label_offset[0],
240                             thisY + self.width/2.0 + self.data_label_offset[1],
241                             "/vM/hL" + pychart_util.apply_format(self.data_label_format, (pair[self.bcol], pair[self.hcol]), 1))
242
243     def get_legend_entry(self):
244         if self.label:
245             return legend.Entry(line_style=(self.legend_line_style or self.line_style),
246                                 fill_style=(self.legend_fill_style or self.fill_style),
247                                 label=self.label)
248         return None
249         
250     def draw(self, ar, can):
251         self.type_check()
252         can.clip(ar.loc[0], ar.loc[1],
253                 ar.loc[0] + ar.size[0], ar.loc[1] + ar.size[1])
254             
255         if self.direction == "vertical":
256             self.draw_vertical(ar, can)
257         else:
258             self.draw_horizontal(ar, can)
259
260         can.endclip()
261
262
263 def init():
264     global fill_styles
265     fill_styles = fill_style.standards.iterate()
266     
267 theme.add_reinitialization_hook(init)
268