root/trunk/pycha/pie.py @ 175

Revision 175, 7.1 kB (checked in by lgs, 18 months ago)

Allow to specify the font of the ticks. Based on a patch by ged_AT_openhex.org. Fixes #26

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
18import math
19
20import cairo
21
22from pycha.chart import Chart, Option
23from pycha.color import hex2rgb
24
25
26class PieChart(Chart):
27
28    def __init__(self, surface=None, options={}):
29        super(PieChart, self).__init__(surface, options)
30        self.slices = []
31        self.centerx = 0
32        self.centery = 0
33        self.radius = 0
34
35    def _updateChart(self):
36        """Evaluates measures for pie charts"""
37        self.centerx = self.area.x + self.area.w * 0.5
38        self.centery = self.area.y + self.area.h * 0.5
39        self.radius = min(self.area.w * self.options.pieRadius,
40                          self.area.h * self.options.pieRadius)
41
42        slices = [dict(name=key,
43                       value=(i, value[0][1]))
44                  for i, (key, value) in enumerate(self.datasets)]
45
46        s = float(sum([slice['value'][1] for slice in slices]))
47
48        fraction = angle = 0.0
49
50        self.slices = []
51        for slice in slices:
52            angle += fraction
53            if slice['value'][1] > 0:
54                fraction = slice['value'][1] / s
55                self.slices.append(Slice(slice['name'], fraction,
56                                         slice['value'][0], slice['value'][1],
57                                         angle))
58
59    def _updateTicks(self):
60        """Evaluates pie ticks"""
61        self.xticks = []
62        if self.options.axis.x.ticks:
63            lookup = dict([(slice.xval, slice) for slice in self.slices])
64            for tick in self.options.axis.x.ticks:
65                if not isinstance(tick, Option):
66                    tick = Option(tick)
67                slice = lookup.get(tick.v, None)
68                label = tick.label or str(tick.v)
69                if slice is not None:
70                    label += ' (%.1f%%)' % (slice.fraction * 100)
71                    self.xticks.append((tick.v, label))
72        else:
73            for slice in self.slices:
74                label = '%s (%.1f%%)' % (slice.name, slice.fraction * 100)
75                self.xticks.append((slice.xval, label))
76
77    def _renderBackground(self, cx):
78        """Renders the background of the chart"""
79        if self.options.background.hide:
80            return
81
82        cx.save()
83
84        if self.options.background.baseColor:
85            cx.set_source_rgb(*hex2rgb(self.options.background.baseColor))
86            x, y, w, h = 0, 0, self.area.w, self.area.h
87            w += self.options.padding.left + self.options.padding.right
88            h += self.options.padding.top + self.options.padding.bottom
89            cx.rectangle(x, y, w, h)
90            cx.fill()
91
92        cx.restore()
93
94    def _renderChart(self, cx):
95        """Renders a pie chart"""
96        cx.set_line_join(cairo.LINE_JOIN_ROUND)
97
98        if self.options.stroke.shadow:
99            cx.save()
100            cx.set_source_rgba(0, 0, 0, 0.15)
101
102            cx.new_path()
103            cx.move_to(self.centerx, self.centery)
104            cx.arc(self.centerx + 1, self.centery + 2, self.radius + 1, 0,
105                   math.pi * 2)
106            cx.line_to(self.centerx, self.centery)
107            cx.close_path()
108            cx.fill()
109            cx.restore()
110
111        cx.save()
112        for slice in self.slices:
113            if slice.isBigEnough():
114                cx.set_source_rgb(*self.colorScheme[slice.name])
115                if self.options.shouldFill:
116                    slice.draw(cx, self.centerx, self.centery, self.radius)
117                    cx.fill()
118
119                if not self.options.stroke.hide:
120                    slice.draw(cx, self.centerx, self.centery, self.radius)
121                    cx.set_line_width(self.options.stroke.width)
122                    cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
123                    cx.stroke()
124
125        cx.restore()
126
127    def _renderAxis(self, cx):
128        """Renders the axis for pie charts"""
129        if self.options.axis.x.hide or not self.xticks:
130            return
131
132        self.xlabels = []
133        lookup = dict([(slice.xval, slice) for slice in self.slices])
134
135
136        cx.select_font_face(self.options.axis.tickFont,
137                            cairo.FONT_SLANT_NORMAL,
138                            cairo.FONT_WEIGHT_NORMAL)
139        cx.set_font_size(self.options.axis.tickFontSize)
140
141        cx.set_source_rgb(*hex2rgb(self.options.axis.labelColor))
142
143        for tick in self.xticks:
144            slice = lookup[tick[0]]
145
146            normalisedAngle = slice.getNormalisedAngle()
147
148            big_radius = self.radius + 10
149            labelx = self.centerx + math.sin(normalisedAngle) * big_radius
150            labely = self.centery - math.cos(normalisedAngle) * big_radius
151
152            label = tick[1]
153            extents = cx.text_extents(label)
154            labelWidth = extents[2]
155            labelHeight = extents[3]
156            x = y = 0
157
158            if normalisedAngle <= math.pi * 0.5:
159                x = labelx
160                y = labely - labelHeight
161            elif math.pi * 0.5 < normalisedAngle <= math.pi:
162                x = labelx
163                y = labely
164            elif math.pi < normalisedAngle <= math.pi * 1.5:
165                x = labelx - labelWidth
166                y = labely
167            else:
168                x = labelx - labelWidth
169                y = labely - labelHeight
170
171            # draw label with text tick[1]
172            cx.move_to(x, y)
173            cx.show_text(label)
174            self.xlabels.append(label)
175
176
177class Slice(object):
178
179    def __init__(self, name, fraction, xval, yval, angle):
180        self.name = name
181        self.fraction = fraction
182        self.xval = xval
183        self.yval = yval
184        self.startAngle = 2 * angle * math.pi
185        self.endAngle = 2 * (angle + fraction) * math.pi
186
187    def __str__(self):
188        return ("<pycha.pie.Slice from %.2f to %.2f (%.2f%%)>" %
189                (self.startAngle, self.endAngle, self.fraction))
190
191    def isBigEnough(self):
192        return abs(self.startAngle - self.endAngle) > 0.001
193
194    def draw(self, cx, centerx, centery, radius):
195        cx.new_path()
196        cx.move_to(centerx, centery)
197        cx.arc(centerx, centery, radius,
198               self.startAngle - math.pi/2,
199               self.endAngle - math.pi/2)
200        cx.line_to(centerx, centery)
201        cx.close_path()
202
203    def getNormalisedAngle(self):
204        normalisedAngle = (self.startAngle + self.endAngle) / 2
205
206        if normalisedAngle > math.pi * 2:
207            normalisedAngle -= math.pi * 2
208        elif normalisedAngle < 0:
209            normalisedAngle += math.pi * 2
210
211        return normalisedAngle
Note: See TracBrowser for help on using the browser.