1# Copyright(c) 2007-2019 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 20from pycha.utils import safe_unicode 21 22 23class BarChart(Chart): 24 25 def __init__(self, surface=None, options={}, debug=False): 26 super(BarChart, self).__init__(surface, options, debug) 27 self.bars = [] 28 self.minxdelta = 0.0 29 self.barWidthForSet = 0.0 30 self.barMargin = 0.0 31 32 def _updateXY(self): 33 super(BarChart, self)._updateXY() 34 # each dataset is centered around a line segment. that's why we 35 # need n + 1 divisions on the x axis 36 self.xscale = 1 / (self.xrange + 1.0) 37 38 def _updateChart(self): 39 """Evaluates measures for vertical bars""" 40 stores = self._getDatasetsValues() 41 uniqx = uniqueIndices(stores) 42 43 if len(uniqx) == 1: 44 self.minxdelta = 1.0 45 else: 46 self.minxdelta = min([abs(uniqx[j] - uniqx[j - 1]) 47 for j in range(1, len(uniqx))]) 48 49 k = self.minxdelta * self.xscale 50 barWidth = k * self.options.barWidthFillFraction 51 self.barWidthForSet = barWidth / len(stores) 52 self.barMargin = k * (1.0 - self.options.barWidthFillFraction) / 2 53 54 self.bars = [] 55 56 def _renderChart(self, cx): 57 """Renders a horizontal/vertical bar chart""" 58 59 def drawBar(bar): 60 stroke_width = self.options.stroke.width 61 ux, uy = cx.device_to_user_distance(stroke_width, stroke_width) 62 if ux < uy: 63 ux = uy 64 cx.set_line_width(ux) 65 66 # gather bar proportions 67 x = self.layout.chart.x + self.layout.chart.w * bar.x 68 y = self.layout.chart.y + self.layout.chart.h * bar.y 69 w = self.layout.chart.w * bar.w 70 h = self.layout.chart.h * bar.h 71 72 if (w < 1 or h < 1) and self.options.yvals.skipSmallValues: 73 return # don't draw when the bar is too small 74 75 if self.options.stroke.shadow: 76 cx.set_source_rgba(0, 0, 0, 0.15) 77 rectangle = self._getShadowRectangle(x, y, w, h) 78 cx.rectangle(*rectangle) 79 cx.fill() 80 81 if self.options.shouldFill or (not self.options.stroke.hide): 82 83 if self.options.shouldFill: 84 cx.set_source_rgb(*self.colorScheme[bar.name]) 85 cx.rectangle(x, y, w, h) 86 cx.fill() 87 88 if not self.options.stroke.hide: 89 cx.set_source_rgb(*hex2rgb(self.options.stroke.color)) 90 cx.rectangle(x, y, w, h) 91 cx.stroke() 92 93 if bar.yerr: 94 self._renderError(cx, x, y, w, h, bar.yval, bar.yerr) 95 96 # render yvals above/beside bars 97 if self.options.yvals.show: 98 cx.save() 99 cx.set_font_size(self.options.yvals.fontSize) 100 cx.set_source_rgb(*hex2rgb(self.options.yvals.fontColor)) 101 102 if callable(self.options.yvals.renderer): 103 label = safe_unicode(self.options.yvals.renderer(bar), 104 self.options.encoding) 105 else: 106 label = safe_unicode(bar.yval, self.options.encoding) 107 extents = cx.text_extents(label) 108 labelW = extents[2] 109 labelH = extents[3] 110 111 self._renderYVal(cx, label, labelW, labelH, x, y, w, h) 112 113 cx.restore() 114 115 cx.save() 116 for bar in self.bars: 117 drawBar(bar) 118 cx.restore() 119 120 def _renderYVal(self, cx, label, width, height, x, y, w, h): 121 raise NotImplementedError 122 123 124class VerticalBarChart(BarChart): 125 126 def _updateChart(self): 127 """Evaluates measures for vertical bars""" 128 super(VerticalBarChart, self)._updateChart() 129 for i, (name, store) in enumerate(self.datasets): 130 for item in store: 131 if len(item) == 3: 132 xval, yval, _ = item 133 else: 134 xval, yval = item 135 136 x = (((xval - self.minxval) * self.xscale) + self.barMargin + (i * self.barWidthForSet)) 137 w = self.barWidthForSet 138 h = abs(yval) * self.yscale 139 if yval > 0: 140 y = (1.0 - h) - self.origin 141 else: 142 y = 1 - self.origin 143 rect = Rect(x, y, w, h, xval, yval, name) 144 145 if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0): 146 self.bars.append(rect) 147 148 def _updateTicks(self): 149 """Evaluates bar ticks""" 150 super(BarChart, self)._updateTicks() 151 offset = (self.minxdelta * self.xscale) / 2 152 self.xticks = [(tick[0] + offset, tick[1]) for tick in self.xticks] 153 154 def _getShadowRectangle(self, x, y, w, h): 155 return (x - 2, y - 2, w + 4, h + 2) 156 157 def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH): 158 x = barX + (barW / 2.0) - (labelW / 2.0) 159 if self.options.yvals.snapToOrigin: 160 y = barY + barH - 0.5 * labelH 161 elif self.options.yvals.inside: 162 y = barY + (1.5 * labelH) 163 else: 164 y = barY - 0.5 * labelH 165 166 # if the label doesn't fit below the bar, put it above the bar 167 if y > (barY + barH): 168 y = barY - 0.5 * labelH 169 170 cx.move_to(x, y) 171 cx.show_text(label) 172 173 def _renderError(self, cx, barX, barY, barW, barH, value, error): 174 center = barX + (barW / 2.0) 175 errorWidth = max(barW * 0.1, 5.0) 176 left = center - errorWidth 177 right = center + errorWidth 178 errorSize = barH * error / value 179 top = barY + errorSize 180 bottom = barY - errorSize 181 182 cx.set_source_rgb(0, 0, 0) 183 cx.move_to(left, top) 184 cx.line_to(right, top) 185 cx.stroke() 186 cx.move_to(center, top) 187 cx.line_to(center, bottom) 188 cx.stroke() 189 cx.move_to(left, bottom) 190 cx.line_to(right, bottom) 191 cx.stroke() 192 193 194class HorizontalBarChart(BarChart): 195 196 def _updateChart(self): 197 """Evaluates measures for horizontal bars""" 198 super(HorizontalBarChart, self)._updateChart() 199 200 for i, (name, store) in enumerate(self.datasets): 201 for item in store: 202 if len(item) == 3: 203 xval, yval, yerr = item 204 else: 205 xval, yval = item 206 yerr = 0.0 207 208 y = (((xval - self.minxval) * self.xscale) + self.barMargin + (i * self.barWidthForSet)) 209 h = self.barWidthForSet 210 w = abs(yval) * self.yscale 211 if yval > 0: 212 x = self.origin 213 else: 214 x = self.origin - w 215 rect = Rect(x, y, w, h, xval, yval, name, yerr) 216 217 if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0): 218 self.bars.append(rect) 219 220 def _updateTicks(self): 221 """Evaluates bar ticks""" 222 super(BarChart, self)._updateTicks() 223 offset = (self.minxdelta * self.xscale) / 2 224 tmp = self.xticks 225 self.xticks = [(1.0 - tick[0], tick[1]) for tick in self.yticks] 226 self.yticks = [(tick[0] + offset, tick[1]) for tick in tmp] 227 228 def _renderLines(self, cx): 229 """Aux function for _renderBackground""" 230 if self.options.axis.y.showLines and self.yticks: 231 for tick in self.xticks: 232 self._renderLine(cx, tick, True) 233 if self.options.axis.x.showLines and self.xticks: 234 for tick in self.yticks: 235 self._renderLine(cx, tick, False) 236 237 def _getShadowRectangle(self, x, y, w, h): 238 return (x, y - 2, w + 2, h + 4) 239 240 def _renderXAxisLabel(self, cx, labelText): 241 labelText = self.options.axis.x.label 242 super(HorizontalBarChart, self)._renderYAxisLabel(cx, labelText) 243 244 def _renderXAxis(self, cx): 245 """Draws the horizontal line representing the X axis""" 246 cx.new_path() 247 cx.move_to(self.layout.chart.x, 248 self.layout.chart.y + self.layout.chart.h) 249 cx.line_to(self.layout.chart.x + self.layout.chart.w, 250 self.layout.chart.y + self.layout.chart.h) 251 cx.close_path() 252 cx.stroke() 253 254 def _renderYAxisLabel(self, cx, labelText): 255 labelText = self.options.axis.y.label 256 super(HorizontalBarChart, self)._renderXAxisLabel(cx, labelText) 257 258 def _renderYAxis(self, cx): 259 # draws the vertical line representing the Y axis 260 cx.new_path() 261 cx.move_to(self.layout.chart.x + self.origin * self.layout.chart.w, 262 self.layout.chart.y) 263 cx.line_to(self.layout.chart.x + self.origin * self.layout.chart.w, 264 self.layout.chart.y + self.layout.chart.h) 265 cx.close_path() 266 cx.stroke() 267 268 def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH): 269 y = barY + (barH / 2.0) + (labelH / 2.0) 270 if self.options.yvals.snapToOrigin: 271 x = barX + 2 272 elif self.options.yvals.inside: 273 x = barX + barW - (1.2 * labelW) 274 else: 275 x = barX + barW + 0.2 * labelW 276 277 # if the label doesn't fit to the left of the bar, put it to the right 278 if x < barX: 279 x = barX + barW + 0.2 * labelW 280 281 cx.move_to(x, y) 282 cx.show_text(label) 283 284 def _renderError(self, cx, barX, barY, barW, barH, value, error): 285 center = barY + (barH / 2.0) 286 errorHeight = max(barH * 0.1, 5.0) 287 top = center + errorHeight 288 bottom = center - errorHeight 289 errorSize = barW * error / value 290 right = barX + barW + errorSize 291 left = barX + barW - errorSize 292 293 cx.set_source_rgb(0, 0, 0) 294 cx.move_to(left, top) 295 cx.line_to(left, bottom) 296 cx.stroke() 297 cx.move_to(left, center) 298 cx.line_to(right, center) 299 cx.stroke() 300 cx.move_to(right, top) 301 cx.line_to(right, bottom) 302 cx.stroke() 303 304 305class Rect(object): 306 307 def __init__(self, x, y, w, h, xval, yval, name, yerr=0.0): 308 self.x, self.y, self.w, self.h = x, y, w, h 309 self.xval, self.yval, self.yerr = xval, yval, yerr 310 self.name = name 311 312 def __str__(self): 313 return ("<pycha.bar.Rect@(%.2f, %.2f) %.2fx%.2f (%.2f, %.2f, %.2f) %s>" 314 % (self.x, self.y, self.w, self.h, 315 self.xval, self.yval, self.yerr, 316 self.name)) 317