root/tags/0.5.0/pycha/bar.py

Revision 177, 8.3 kB (checked in by lgs, 3 years ago)

It looks like Cairo do unexpected things with the order of rectangle, fill and set_source_rgb. Fixes #25

Line 
1# Copyright(c) 2007-2009 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
2#
3# This file is part of PyCha.
4#
5# PyCha is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# PyCha is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public License
16# along with PyCha.  If not, see <http://www.gnu.org/licenses/>.
17
18from pycha.chart import Chart, uniqueIndices
19from pycha.color import hex2rgb
20
21
22class BarChart(Chart):
23
24    def __init__(self, surface=None, options={}):
25        super(BarChart, self).__init__(surface, options)
26        self.bars = []
27        self.minxdelta = 0.0
28        self.barWidthForSet = 0.0
29        self.barMargin = 0.0
30
31    def _updateXY(self):
32        super(BarChart, self)._updateXY()
33        # each dataset is centered around a line segment. that's why we
34        # need n + 1 divisions on the x axis
35        self.xscale = 1 / (self.xrange + 1.0)
36
37    def _updateChart(self):
38        """Evaluates measures for vertical bars"""
39        stores = self._getDatasetsValues()
40        uniqx = uniqueIndices(stores)
41
42        if len(uniqx) == 1:
43            self.minxdelta = 1.0
44        else:
45            self.minxdelta = min([abs(uniqx[j] - uniqx[j-1])
46                                  for j in range(1, len(uniqx))])
47
48        k = self.minxdelta * self.xscale
49        barWidth = k * self.options.barWidthFillFraction
50        self.barWidthForSet = barWidth / len(stores)
51        self.barMargin = k * (1.0 - self.options.barWidthFillFraction) / 2
52
53        self.bars = []
54
55    def _renderChart(self, cx):
56        """Renders a horizontal/vertical bar chart"""
57
58        def drawBar(bar):
59            stroke_width = self.options.stroke.width
60            ux, uy = cx.device_to_user_distance(stroke_width, stroke_width)
61            if ux < uy:
62                ux = uy
63            cx.set_line_width(ux)
64
65            # gather bar proportions
66            x = self.area.x + self.area.w * bar.x
67            y = self.area.y + self.area.h * bar.y
68            w = self.area.w * bar.w
69            h = self.area.h * bar.h
70
71            if w < 1 or h < 1:
72                return # don't draw when the bar is too small
73
74            if self.options.stroke.shadow:
75                cx.set_source_rgba(0, 0, 0, 0.15)
76                rectangle = self._getShadowRectangle(x, y, w, h)
77                cx.rectangle(*rectangle)
78                cx.fill()
79
80            if self.options.shouldFill or (not self.options.stroke.hide):
81
82                if self.options.shouldFill:
83                    cx.set_source_rgb(*self.colorScheme[bar.name])
84                    cx.rectangle(x, y, w, h)
85                    cx.fill()
86
87                if not self.options.stroke.hide:
88                    cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
89                    cx.rectangle(x, y, w, h)
90                    cx.stroke()
91
92            # render yvals above/beside bars
93            if self.options.yvals.show:
94                cx.save()
95                cx.set_font_size(self.options.yvals.fontSize)
96                cx.set_source_rgb(*hex2rgb(self.options.yvals.fontColor))
97
98                label = unicode(bar.yval)
99                extents = cx.text_extents(label)
100                labelW = extents[2]
101                labelH = extents[3]
102
103                self._renderYVal(cx, label, labelW, labelH, x, y, w, h)
104
105                cx.restore()
106
107        cx.save()
108        for bar in self.bars:
109            drawBar(bar)
110        cx.restore()
111
112    def _renderYVal(self, cx, label, width, height, x, y, w, h):
113        raise NotImplementedError
114
115
116class VerticalBarChart(BarChart):
117
118    def _updateChart(self):
119        """Evaluates measures for vertical bars"""
120        super(VerticalBarChart, self)._updateChart()
121        for i, (name, store) in enumerate(self.datasets):
122            for item in store:
123                xval, yval = item
124                x = (((xval - self.minxval) * self.xscale)
125                    + self.barMargin + (i * self.barWidthForSet))
126                w = self.barWidthForSet
127                h = abs(yval) * self.yscale
128                if yval > 0:
129                    y = (1.0 - h) - self.area.origin
130                else:
131                    y = 1 - self.area.origin
132                rect = Rect(x, y, w, h, xval, yval, name)
133
134                if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
135                    self.bars.append(rect)
136
137    def _updateTicks(self):
138        """Evaluates bar ticks"""
139        super(BarChart, self)._updateTicks()
140        offset = (self.minxdelta * self.xscale) / 2
141        self.xticks = [(tick[0] + offset, tick[1]) for tick in self.xticks]
142
143    def _getShadowRectangle(self, x, y, w, h):
144        return (x-2, y-2, w+4, h+2)
145
146    def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH):
147        x = barX + (barW / 2.0) - (labelW / 2.0)
148        if self.options.yvals.inside:
149            y = barY + (1.5 * labelH)
150        else:
151            y = barY - 0.5 * labelH
152
153        # if the label doesn't fit below the bar, put it above the bar
154        if y > (barY + barH):
155            y = barY - 0.5 * labelH
156
157        cx.move_to(x, y)
158        cx.show_text(label)
159
160
161class HorizontalBarChart(BarChart):
162
163    def _updateChart(self):
164        """Evaluates measures for horizontal bars"""
165        super(HorizontalBarChart, self)._updateChart()
166
167        for i, (name, store) in enumerate(self.datasets):
168            for item in store:
169                xval, yval = item
170                y = (((xval - self.minxval) * self.xscale)
171                     + self.barMargin + (i * self.barWidthForSet))
172                h = self.barWidthForSet
173                w = abs(yval) * self.yscale
174                if yval > 0:
175                    x = self.area.origin
176                else:
177                    x = self.area.origin - w
178                rect = Rect(x, y, w, h, xval, yval, name)
179
180                if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
181                    self.bars.append(rect)
182
183    def _updateTicks(self):
184        """Evaluates bar ticks"""
185        super(BarChart, self)._updateTicks()
186        offset = (self.minxdelta * self.xscale) / 2
187        tmp = self.xticks
188        self.xticks = [(1.0 - tick[0], tick[1]) for tick in self.yticks]
189        self.yticks = [(tick[0] + offset, tick[1]) for tick in tmp]
190
191    def _renderLines(self, cx):
192        """Aux function for _renderBackground"""
193        ticks = self.xticks
194        for tick in ticks:
195            self._renderLine(cx, tick, True)
196
197    def _getShadowRectangle(self, x, y, w, h):
198        return (x, y-2, w+2, h+4)
199
200    def _renderXAxis(self, cx):
201        """Draws the horizontal line representing the X axis"""
202        cx.new_path()
203        cx.move_to(self.area.x, self.area.y + self.area.h)
204        cx.line_to(self.area.x + self.area.w, self.area.y + self.area.h)
205        cx.close_path()
206        cx.stroke()
207
208    def _renderYAxis(self, cx):
209        # draws the vertical line representing the Y axis
210        cx.new_path()
211        cx.move_to(self.area.x + self.area.origin * self.area.w,
212                   self.area.y)
213        cx.line_to(self.area.x + self.area.origin * self.area.w,
214                   self.area.y + self.area.h)
215        cx.close_path()
216        cx.stroke()
217
218    def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH):
219        y = barY + (barH / 2.0) + (labelH / 2.0)
220        if self.options.yvals.inside:
221            x = barX + barW - (1.2 * labelW)
222        else:
223            x = barX + barW + 0.2 * labelW
224
225        # if the label doesn't fit to the left of the bar, put it to the right
226        if x < barX:
227            x = barX + barW + 0.2 * labelW
228
229        cx.move_to(x, y)
230        cx.show_text(label)
231
232
233class Rect(object):
234
235    def __init__(self, x, y, w, h, xval, yval, name):
236        self.x, self.y, self.w, self.h = x, y, w, h
237        self.xval, self.yval = xval, yval
238        self.name = name
239
240    def __str__(self):
241        return ("<pycha.bar.Rect@(%.2f, %.2f) %.2fx%.2f (%.2f, %.2f) %s>"
242                % (self.x, self.y, self.w, self.h, self.xval, self.yval,
243                   self.name))
Note: See TracBrowser for help on using the browser.