12cf2e8247
Add IsEvadedOffense to EFPenalty Fix remote log reading in not Windows
424 lines
12 KiB
Python
424 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# This file is part of pygal
|
|
#
|
|
# A python svg graph plotting library
|
|
# Copyright © 2012-2016 Kozea
|
|
#
|
|
# This library is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Lesser General Public License as published by the Free
|
|
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
# later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""Projection and bounding helpers"""
|
|
|
|
from __future__ import division
|
|
|
|
from math import cos, log10, pi, sin
|
|
|
|
|
|
class Margin(object):
|
|
|
|
"""Class reprensenting a margin (top, right, left, bottom)"""
|
|
|
|
def __init__(self, top, right, bottom, left):
|
|
"""Create the margin object from the top, right, left, bottom margin"""
|
|
self.top = top
|
|
self.right = right
|
|
self.bottom = bottom
|
|
self.left = left
|
|
|
|
@property
|
|
def x(self):
|
|
"""Helper for total x margin"""
|
|
return self.left + self.right
|
|
|
|
@property
|
|
def y(self):
|
|
"""Helper for total y margin"""
|
|
return self.top + self.bottom
|
|
|
|
|
|
class Box(object):
|
|
|
|
"""Chart boundings"""
|
|
|
|
margin = .02
|
|
|
|
def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1):
|
|
"""
|
|
Create the chart bounds with min max horizontal
|
|
and vertical values
|
|
"""
|
|
self._xmin = xmin
|
|
self._ymin = ymin
|
|
self._xmax = xmax
|
|
self._ymax = ymax
|
|
|
|
def set_polar_box(self, rmin=0, rmax=1, tmin=0, tmax=2 * pi):
|
|
"""Helper for polar charts"""
|
|
self._rmin = rmin
|
|
self._rmax = rmax
|
|
self._tmin = tmin
|
|
self._tmax = tmax
|
|
self.xmin = self.ymin = rmin - rmax
|
|
self.xmax = self.ymax = rmax - rmin
|
|
|
|
@property
|
|
def xmin(self):
|
|
"""X minimum getter"""
|
|
return self._xmin
|
|
|
|
@xmin.setter
|
|
def xmin(self, value):
|
|
"""X minimum setter"""
|
|
if value is not None:
|
|
self._xmin = value
|
|
|
|
@property
|
|
def ymin(self):
|
|
"""Y minimum getter"""
|
|
return self._ymin
|
|
|
|
@ymin.setter
|
|
def ymin(self, value):
|
|
"""Y minimum setter"""
|
|
if value is not None:
|
|
self._ymin = value
|
|
|
|
@property
|
|
def xmax(self):
|
|
"""X maximum getter"""
|
|
return self._xmax
|
|
|
|
@xmax.setter
|
|
def xmax(self, value):
|
|
"""X maximum setter"""
|
|
if value is not None:
|
|
self._xmax = value
|
|
|
|
@property
|
|
def ymax(self):
|
|
"""Y maximum getter"""
|
|
return self._ymax
|
|
|
|
@ymax.setter
|
|
def ymax(self, value):
|
|
"""Y maximum setter"""
|
|
if value or self.ymin:
|
|
self._ymax = value
|
|
|
|
@property
|
|
def width(self):
|
|
"""Helper for box width"""
|
|
return self.xmax - self.xmin
|
|
|
|
@property
|
|
def height(self):
|
|
"""Helper for box height"""
|
|
return self.ymax - self.ymin
|
|
|
|
def swap(self):
|
|
"""Return the box (for horizontal graphs)"""
|
|
self.xmin, self.ymin = self.ymin, self.xmin
|
|
self.xmax, self.ymax = self.ymax, self.xmax
|
|
|
|
def fix(self, with_margin=True):
|
|
"""Correct box when no values and take margin in account"""
|
|
if not self.width:
|
|
self.xmax = self.xmin + 1
|
|
if not self.height:
|
|
self.ymin /= 2
|
|
self.ymax += self.ymin
|
|
xmargin = self.margin * self.width
|
|
self.xmin -= xmargin
|
|
self.xmax += xmargin
|
|
if with_margin:
|
|
ymargin = self.margin * self.height
|
|
self.ymin -= ymargin
|
|
self.ymax += ymargin
|
|
|
|
|
|
class View(object):
|
|
|
|
"""Projection base class"""
|
|
|
|
def __init__(self, width, height, box):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
self.width = width
|
|
self.height = height
|
|
self.box = box
|
|
self.box.fix()
|
|
|
|
def x(self, x):
|
|
"""Project x"""
|
|
if x is None:
|
|
return None
|
|
return self.width * (x - self.box.xmin) / self.box.width
|
|
|
|
def y(self, y):
|
|
"""Project y"""
|
|
if y is None:
|
|
return None
|
|
return (self.height - self.height *
|
|
(y - self.box.ymin) / self.box.height)
|
|
|
|
def __call__(self, xy):
|
|
"""Project x and y"""
|
|
x, y = xy
|
|
return (self.x(x), self.y(y))
|
|
|
|
|
|
class ReverseView(View):
|
|
|
|
"""Same as view but reversed vertically"""
|
|
|
|
def y(self, y):
|
|
"""Project reversed y"""
|
|
if y is None:
|
|
return None
|
|
return (self.height * (y - self.box.ymin) / self.box.height)
|
|
|
|
|
|
class HorizontalView(View):
|
|
|
|
"""Same as view but transposed"""
|
|
|
|
def __init__(self, width, height, box):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
self._force_vertical = None
|
|
self.width = width
|
|
self.height = height
|
|
|
|
self.box = box
|
|
self.box.fix()
|
|
self.box.swap()
|
|
|
|
def x(self, x):
|
|
"""Project x as y"""
|
|
if x is None:
|
|
return None
|
|
if self._force_vertical:
|
|
return super(HorizontalView, self).x(x)
|
|
return super(HorizontalView, self).y(x)
|
|
|
|
def y(self, y):
|
|
"""Project y as x"""
|
|
if y is None:
|
|
return None
|
|
if self._force_vertical:
|
|
return super(HorizontalView, self).y(y)
|
|
return super(HorizontalView, self).x(y)
|
|
|
|
|
|
class PolarView(View):
|
|
|
|
"""Polar projection for pie like graphs"""
|
|
|
|
def __call__(self, rhotheta):
|
|
"""Project rho and theta"""
|
|
if None in rhotheta:
|
|
return None, None
|
|
rho, theta = rhotheta
|
|
return super(PolarView, self).__call__(
|
|
(rho * cos(theta), rho * sin(theta)))
|
|
|
|
|
|
class PolarLogView(View):
|
|
|
|
"""Logarithmic polar projection"""
|
|
|
|
def __init__(self, width, height, box):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
super(PolarLogView, self).__init__(width, height, box)
|
|
if not hasattr(box, '_rmin') or not hasattr(box, '_rmax'):
|
|
raise Exception(
|
|
'Box must be set with set_polar_box for polar charts')
|
|
|
|
self.log10_rmax = log10(self.box._rmax)
|
|
self.log10_rmin = log10(self.box._rmin)
|
|
if self.log10_rmin == self.log10_rmax:
|
|
self.log10_rmax = self.log10_rmin + 1
|
|
|
|
def __call__(self, rhotheta):
|
|
"""Project rho and theta"""
|
|
if None in rhotheta:
|
|
return None, None
|
|
rho, theta = rhotheta
|
|
# Center case
|
|
if rho == 0:
|
|
return super(PolarLogView, self).__call__((0, 0))
|
|
rho = (self.box._rmax - self.box._rmin) * (
|
|
log10(rho) - self.log10_rmin) / (
|
|
self.log10_rmax - self.log10_rmin)
|
|
return super(PolarLogView, self).__call__(
|
|
(rho * cos(theta), rho * sin(theta)))
|
|
|
|
|
|
class PolarThetaView(View):
|
|
|
|
"""Logarithmic polar projection"""
|
|
|
|
def __init__(self, width, height, box, aperture=pi / 3):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
super(PolarThetaView, self).__init__(width, height, box)
|
|
self.aperture = aperture
|
|
if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'):
|
|
raise Exception(
|
|
'Box must be set with set_polar_box for polar charts')
|
|
|
|
def __call__(self, rhotheta):
|
|
"""Project rho and theta"""
|
|
if None in rhotheta:
|
|
return None, None
|
|
rho, theta = rhotheta
|
|
start = 3 * pi / 2 + self.aperture / 2
|
|
theta = start + (2 * pi - self.aperture) * (
|
|
theta - self.box._tmin) / (
|
|
self.box._tmax - self.box._tmin)
|
|
return super(PolarThetaView, self).__call__(
|
|
(rho * cos(theta), rho * sin(theta)))
|
|
|
|
|
|
class PolarThetaLogView(View):
|
|
|
|
"""Logarithmic polar projection"""
|
|
|
|
def __init__(self, width, height, box, aperture=pi / 3):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
super(PolarThetaLogView, self).__init__(width, height, box)
|
|
self.aperture = aperture
|
|
if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'):
|
|
raise Exception(
|
|
'Box must be set with set_polar_box for polar charts')
|
|
self.log10_tmax = log10(self.box._tmax) if self.box._tmax > 0 else 0
|
|
self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0
|
|
if self.log10_tmin == self.log10_tmax:
|
|
self.log10_tmax = self.log10_tmin + 1
|
|
|
|
def __call__(self, rhotheta):
|
|
"""Project rho and theta"""
|
|
if None in rhotheta:
|
|
return None, None
|
|
rho, theta = rhotheta
|
|
# Center case
|
|
if theta == 0:
|
|
return super(PolarThetaLogView, self).__call__((0, 0))
|
|
theta = self.box._tmin + (self.box._tmax - self.box._tmin) * (
|
|
log10(theta) - self.log10_tmin) / (
|
|
self.log10_tmax - self.log10_tmin)
|
|
|
|
start = 3 * pi / 2 + self.aperture / 2
|
|
theta = start + (2 * pi - self.aperture) * (
|
|
theta - self.box._tmin) / (
|
|
self.box._tmax - self.box._tmin)
|
|
|
|
return super(PolarThetaLogView, self).__call__(
|
|
(rho * cos(theta), rho * sin(theta)))
|
|
|
|
|
|
class LogView(View):
|
|
|
|
"""Y Logarithmic projection"""
|
|
|
|
# Do not want to call the parent here
|
|
def __init__(self, width, height, box):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
self.width = width
|
|
self.height = height
|
|
self.box = box
|
|
self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0
|
|
self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0
|
|
if self.log10_ymin == self.log10_ymax:
|
|
self.log10_ymax = self.log10_ymin + 1
|
|
self.box.fix(False)
|
|
|
|
def y(self, y):
|
|
"""Project y"""
|
|
if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0:
|
|
return 0
|
|
return (self.height - self.height *
|
|
(log10(y) - self.log10_ymin) / (
|
|
self.log10_ymax - self.log10_ymin))
|
|
|
|
|
|
class XLogView(View):
|
|
|
|
"""X logarithmic projection"""
|
|
|
|
# Do not want to call the parent here
|
|
def __init__(self, width, height, box):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
self.width = width
|
|
self.height = height
|
|
self.box = box
|
|
self.log10_xmax = log10(self.box.xmax) if self.box.xmax > 0 else 0
|
|
self.log10_xmin = log10(self.box.xmin) if self.box.xmin > 0 else 0
|
|
self.box.fix(False)
|
|
|
|
def x(self, x):
|
|
"""Project x"""
|
|
if x is None or x <= 0 or self.log10_xmax - self.log10_xmin == 0:
|
|
return None
|
|
return (self.width *
|
|
(log10(x) - self.log10_xmin) /
|
|
(self.log10_xmax - self.log10_xmin))
|
|
|
|
|
|
class XYLogView(XLogView, LogView):
|
|
|
|
"""X and Y logarithmic projection"""
|
|
|
|
def __init__(self, width, height, box):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
self.width = width
|
|
self.height = height
|
|
self.box = box
|
|
self.log10_ymax = log10(self.box.ymax) if self.box.ymax > 0 else 0
|
|
self.log10_ymin = log10(self.box.ymin) if self.box.ymin > 0 else 0
|
|
self.log10_xmax = log10(self.box.xmax) if self.box.xmax > 0 else 0
|
|
self.log10_xmin = log10(self.box.xmin) if self.box.xmin > 0 else 0
|
|
self.box.fix(False)
|
|
|
|
|
|
class HorizontalLogView(XLogView):
|
|
|
|
"""Transposed Logarithmic projection"""
|
|
|
|
# Do not want to call the parent here
|
|
def __init__(self, width, height, box):
|
|
"""Create the view with a width an height and a box bounds"""
|
|
self._force_vertical = None
|
|
self.width = width
|
|
self.height = height
|
|
self.box = box
|
|
self.log10_xmax = log10(self.box.ymax) if self.box.ymax > 0 else 0
|
|
self.log10_xmin = log10(self.box.ymin) if self.box.ymin > 0 else 0
|
|
if self.log10_xmin == self.log10_xmax:
|
|
self.log10_xmax = self.log10_xmin + 1
|
|
self.box.fix(False)
|
|
self.box.swap()
|
|
|
|
def x(self, x):
|
|
"""Project x as y"""
|
|
if x is None:
|
|
return None
|
|
if self._force_vertical:
|
|
return super(HorizontalLogView, self).x(x)
|
|
return super(XLogView, self).y(x)
|
|
|
|
def y(self, y):
|
|
"""Project y as x"""
|
|
if y is None:
|
|
return None
|
|
if self._force_vertical:
|
|
return super(XLogView, self).y(y)
|
|
return super(HorizontalLogView, self).x(y)
|