1# -*- coding: utf-8 -*- 2# This file is part of pygal 3# 4# A python svg graph plotting library 5# Copyright © 2012-2016 Kozea 6# 7# This library is free software: you can redistribute it and/or modify it under 8# the terms of the GNU Lesser General Public License as published by the Free 9# Software Foundation, either version 3 of the License, or (at your option) any 10# later version. 11# 12# This library is distributed in the hope that it will be useful, but WITHOUT 13# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 15# details. 16# 17# You should have received a copy of the GNU Lesser General Public License 18# along with pygal. If not, see <http://www.gnu.org/licenses/>. 19 20"""Projection and bounding helpers""" 21 22from __future__ import division 23 24from math import cos, log10, pi, sin 25 26 27class Margin(object): 28 29 """Class reprensenting a margin (top, right, left, bottom)""" 30 31 def __init__(self, top, right, bottom, left): 32 """Create the margin object from the top, right, left, bottom margin""" 33 self.top = top 34 self.right = right 35 self.bottom = bottom 36 self.left = left 37 38 @property 39 def x(self): 40 """Helper for total x margin""" 41 return self.left + self.right 42 43 @property 44 def y(self): 45 """Helper for total y margin""" 46 return self.top + self.bottom 47 48 49class Box(object): 50 51 """Chart boundings""" 52 53 margin = .02 54 55 def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1): 56 """ 57 Create the chart bounds with min max horizontal 58 and vertical values 59 """ 60 self._xmin = xmin 61 self._ymin = ymin 62 self._xmax = xmax 63 self._ymax = ymax 64 65 def set_polar_box(self, rmin=0, rmax=1, tmin=0, tmax=2 * pi): 66 """Helper for polar charts""" 67 self._rmin = rmin 68 self._rmax = rmax 69 self._tmin = tmin 70 self._tmax = tmax 71 self.xmin = self.ymin = rmin - rmax 72 self.xmax = self.ymax = rmax - rmin 73 74 @property 75 def xmin(self): 76 """X minimum getter""" 77 return self._xmin 78 79 @xmin.setter 80 def xmin(self, value): 81 """X minimum setter""" 82 if value is not None: 83 self._xmin = value 84 85 @property 86 def ymin(self): 87 """Y minimum getter""" 88 return self._ymin 89 90 @ymin.setter 91 def ymin(self, value): 92 """Y minimum setter""" 93 if value is not None: 94 self._ymin = value 95 96 @property 97 def xmax(self): 98 """X maximum getter""" 99 return self._xmax 100 101 @xmax.setter 102 def xmax(self, value): 103 """X maximum setter""" 104 if value is not None: 105 self._xmax = value 106 107 @property 108 def ymax(self): 109 """Y maximum getter""" 110 return self._ymax 111 112 @ymax.setter 113 def ymax(self, value): 114 """Y maximum setter""" 115 if value or self.ymin: 116 self._ymax = value 117 118 @property 119 def width(self): 120 """Helper for box width""" 121 return self.xmax - self.xmin 122 123 @property 124 def height(self): 125 """Helper for box height""" 126 return self.ymax - self.ymin 127 128 def swap(self): 129 """Return the box (for horizontal graphs)""" 130 self.xmin, self.ymin = self.ymin, self.xmin 131 self.xmax, self.ymax = self.ymax, self.xmax 132 133 def fix(self, with_margin=True): 134 """Correct box when no values and take margin in account""" 135 if not self.width: 136 self.xmax = self.xmin + 1 137 if not self.height: 138 self.ymin /= 2 139 self.ymax += self.ymin 140 xmargin = self.margin * self.width 141 self.xmin -= xmargin 142 self.xmax += xmargin 143 if with_margin: 144 ymargin = self.margin * self.height 145 self.ymin -= ymargin 146 self.ymax += ymargin 147 148 149class View(object): 150 151 """Projection base class""" 152 153 def __init__(self, width, height, box): 154 """Create the view with a width an height and a box bounds""" 155 self.width = width 156 self.height = height 157 self.box = box 158 self.box.fix() 159 160 def x(self, x): 161 """Project x""" 162 if x is None: 163 return None 164 return self.width * (x - self.box.xmin) / self.box.width 165 166 def y(self, y): 167 """Project y""" 168 if y is None: 169 return None 170 return (self.height - self.height * 171 (y - self.box.ymin) / self.box.height) 172 173 def __call__(self, xy): 174 """Project x and y""" 175 x, y = xy 176 return (self.x(x), self.y(y)) 177 178 179class ReverseView(View): 180 181 """Same as view but reversed vertically""" 182 183 def y(self, y): 184 """Project reversed y""" 185 if y is None: 186 return None 187 return (self.height * (y - self.box.ymin) / self.box.height) 188 189 190class HorizontalView(View): 191 192 """Same as view but transposed""" 193 194 def __init__(self, width, height, box): 195 """Create the view with a width an height and a box bounds""" 196 self._force_vertical = None 197 self.width = width 198 self.height = height 199 200 self.box = box 201 self.box.fix() 202 self.box.swap() 203 204 def x(self, x): 205 """Project x as y""" 206 if x is None: 207 return None 208 if self._force_vertical: 209 return super(HorizontalView, self).x(x) 210 return super(HorizontalView, self).y(x) 211 212 def y(self, y): 213 """Project y as x""" 214 if y is None: 215 return None 216 if self._force_vertical: 217 return super(HorizontalView, self).y(y) 218 return super(HorizontalView, self).x(y) 219 220 221class PolarView(View): 222 223 """Polar projection for pie like graphs""" 224 225 def __call__(self, rhotheta): 226 """Project rho and theta""" 227 if None in rhotheta: 228 return None, None 229 rho, theta = rhotheta 230 return super(PolarView, self).__call__( 231 (rho * cos(theta), rho * sin(theta))) 232 233 234class PolarLogView(View): 235 236 """Logarithmic polar projection""" 237 238 def __init__(self, width, height, box): 239 """Create the view with a width an height and a box bounds""" 240 super(PolarLogView, self).__init__(width, height, box) 241 if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'): 242 raise Exception( 243 'Box must be set with set_polar_box for polar charts') 244 245 self.log10_rmax = log10(self.box._rmax) 246 self.log10_rmin = log10(self.box._rmin) 247 if self.log10_rmin == self.log10_rmax: 248 self.log10_rmax = self.log10_rmin + 1 249 250 def __call__(self, rhotheta): 251 """Project rho and theta""" 252 if None in rhotheta: 253 return None, None 254 rho, theta = rhotheta 255 # Center case 256 if rho == 0: 257 return super(PolarLogView, self).__call__((0, 0)) 258 rho = (self.box._rmax - self.box._rmin) * ( 259 log10(rho) - self.log10_rmin) / ( 260 self.log10_rmax - self.log10_rmin) 261 return super(PolarLogView, self).__call__( 262 (rho * cos(theta), rho * sin(theta))) 263 264 265class PolarThetaView(View): 266 267 """Logarithmic polar projection""" 268 269 def __init__(self, width, height, box, aperture=pi / 3): 270 """Create the view with a width an height and a box bounds""" 271 super(PolarThetaView, self).__init__(width, height, box) 272 self.aperture = aperture 273 if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): 274 raise Exception( 275 'Box must be set with set_polar_box for polar charts') 276 277 def __call__(self, rhotheta): 278 """Project rho and theta""" 279 if None in rhotheta: 280 return None, None 281 rho, theta = rhotheta 282 start = 3 * pi / 2 + self.aperture / 2 283 theta = start + (2 * pi - self.aperture) * ( 284 theta - self.box._tmin) / ( 285 self.box._tmax - self.box._tmin) 286 return super(PolarThetaView, self).__call__( 287 (rho * cos(theta), rho * sin(theta))) 288 289 290class PolarThetaLogView(View): 291 292 """Logarithmic polar projection""" 293 294 def __init__(self, width, height, box, aperture=pi / 3): 295 """Create the view with a width an height and a box bounds""" 296 super(PolarThetaLogView, self).__init__(width, height, box) 297 self.aperture = aperture 298 if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): 299 raise Exception( 300 'Box must be set with set_polar_box for polar charts') 301 self.log10_tmax = log10(self.box._tmax) if self.box._tmax > 0 else 0 302 self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0 303 if self.log10_tmin == self.log10_tmax: 304 self.log10_tmax = self.log10_tmin + 1 305 306 def __call__(self, rhotheta): 307 """Project rho and theta""" 308 if None in rhotheta: 309 return None, None 310 rho, theta = rhotheta 311 # Center case 312 if theta == 0: 313 return super(PolarThetaLogView, self).__call__((0, 0)) 314 theta = self.box._tmin + (self.box._tmax - self.box._tmin) * ( 315 log10(theta) - self.log10_tmin) / ( 316 self.log10_tmax - self.log10_tmin) 317 318 start = 3 * pi / 2 + self.aperture / 2 319 theta = start + (2 * pi - self.aperture) * ( 320 theta - self.box._tmin) / ( 321 self.box._tmax - self.box._tmin) 322 323 return super(PolarThetaLogView, self).__call__( 324 (rho * cos(theta), rho * sin(theta))) 325 326 327class LogView(View): 328 329 """Y Logarithmic projection""" 330 331 # Do not want to call the parent here 332 def __init__(self, width, height, box): 333 """Create the view with a width an height and a box bounds""" 334 self.width = width 335 self.height = height 336 self.box = box 337 self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0 338 self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0 339 if self.log10_ymin == self.log10_ymax: 340 self.log10_ymax = self.log10_ymin + 1 341 self.box.fix(False) 342 343 def y(self, y): 344 """Project y""" 345 if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0: 346 return 0 347 return (self.height - self.height * 348 (log10(y) - self.log10_ymin) / ( 349 self.log10_ymax - self.log10_ymin)) 350 351 352class XLogView(View): 353 354 """X logarithmic projection""" 355 356 # Do not want to call the parent here 357 def __init__(self, width, height, box): 358 """Create the view with a width an height and a box bounds""" 359 self.width = width 360 self.height = height 361 self.box = box 362 self.log10_xmax = log10(self.box.xmax) if self.box.xmax > 0 else 0 363 self.log10_xmin = log10(self.box.xmin) if self.box.xmin > 0 else 0 364 self.box.fix(False) 365 366 def x(self, x): 367 """Project x""" 368 if x is None or x <= 0 or self.log10_xmax - self.log10_xmin == 0: 369 return None 370 return (self.width * 371 (log10(x) - self.log10_xmin) / 372 (self.log10_xmax - self.log10_xmin)) 373 374 375class XYLogView(XLogView, LogView): 376 377 """X and Y logarithmic projection""" 378 379 def __init__(self, width, height, box): 380 """Create the view with a width an height and a box bounds""" 381 self.width = width 382 self.height = height 383 self.box = box 384 self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0 385 self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0 386 self.log10_xmax = log10(self.box.xmax) if self.box.xmax > 0 else 0 387 self.log10_xmin = log10(self.box.xmin) if self.box.xmin > 0 else 0 388 self.box.fix(False) 389 390 391class HorizontalLogView(XLogView): 392 393 """Transposed Logarithmic projection""" 394 395 # Do not want to call the parent here 396 def __init__(self, width, height, box): 397 """Create the view with a width an height and a box bounds""" 398 self._force_vertical = None 399 self.width = width 400 self.height = height 401 self.box = box 402 self.log10_xmax = log10(self.box.ymax) if self.box.ymax > 0 else 0 403 self.log10_xmin = log10(self.box.ymin) if self.box.ymin > 0 else 0 404 if self.log10_xmin == self.log10_xmax: 405 self.log10_xmax = self.log10_xmin + 1 406 self.box.fix(False) 407 self.box.swap() 408 409 def x(self, x): 410 """Project x as y""" 411 if x is None: 412 return None 413 if self._force_vertical: 414 return super(HorizontalLogView, self).x(x) 415 return super(XLogView, self).y(x) 416 417 def y(self, y): 418 """Project y as x""" 419 if y is None: 420 return None 421 if self._force_vertical: 422 return super(XLogView, self).y(y) 423 return super(HorizontalLogView, self).x(y) 424