Browse Source

For issue #309 modified Line.py and add a test: No new failure!

pull/342/head
qins 8 years ago
parent
commit
899bad63b0
  1. 13
      pygal/graph/line.py
  2. 215
      pygal/graph/line.py~
  3. 15
      pygal/test/test_line_log_none_max_solved.py
  4. 15
      pygal/test/test_line_log_none_max_solved.py~
  5. 2
      pygal/view.py
  6. 423
      pygal/view.py~

13
pygal/graph/line.py

@ -99,6 +99,12 @@ class Line(Graph):
for i, (x, y) in enumerate(view_values):
if None in (x, y):
continue
#modified
if self.logarithmic:
if points[i][1] == None or points[i][1] <= 0:
continue
if (serie.show_only_major_dots and
self.x_labels and i < len(self.x_labels) and
self.x_labels[i] not in self._x_labels_major):
@ -165,6 +171,13 @@ class Line(Graph):
# plain vanilla rendering
sequences = [view_values]
#modified
if self.logarithmic:
for seq in sequences:
for ele in seq[::-1]:
if points[seq.index(ele)][1]==None or points[seq.index(ele)][1] <= 0:
del seq[seq.index(ele)]
for seq in sequences:
self.svg.line(
serie_node['plot'], seq, close=self._self_close,

215
pygal/graph/line.py~

@ -0,0 +1,215 @@
# -*- 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/>.
"""
Line chart: Display series of data as markers (dots)
connected by straight segments
"""
from __future__ import division
from pygal.graph.graph import Graph
from pygal.util import alter, cached_property, decorate
class Line(Graph):
"""Line graph class"""
def __init__(self, *args, **kwargs):
"""Set _self_close as False, it's True for Radar like Line"""
self._self_close = False
super(Line, self).__init__(*args, **kwargs)
@cached_property
def _values(self):
"""Getter for series values (flattened)"""
return [
val[1]
for serie in self.series
for val in (serie.interpolated
if self.interpolate else serie.points)
if val[1] is not None and (not self.logarithmic or val[1] > 0)]
@cached_property
def _secondary_values(self):
"""Getter for secondary series values (flattened)"""
return [
val[1]
for serie in self.secondary_series
for val in (serie.interpolated
if self.interpolate else serie.points)
if val[1] is not None and (not self.logarithmic or val[1] > 0)]
def _fill(self, values):
"""Add extra values to fill the line"""
zero = self.view.y(min(max(self.zero, self._box.ymin), self._box.ymax))
# Check to see if the data has been padded with "none's"
# Fill doesn't work correctly otherwise
end = len(values) - 1
while end > 0:
x, y = values[end]
if self.missing_value_fill_truncation == "either":
if x is not None and y is not None:
break
elif self.missing_value_fill_truncation == "x":
if x is not None:
break
elif self.missing_value_fill_truncation == "y":
if y is not None:
break
else:
raise ValueError(
"Invalid value ({}) for config key "
"'missing_value_fill_truncation';"
" Use 'x', 'y' or 'either'".format(
self.missing_value_fill_truncation))
end -= 1
return ([(values[0][0], zero)] +
values +
[(values[end][0], zero)])
def line(self, serie, rescale=False):
"""Draw the line serie"""
serie_node = self.svg.serie(serie)
if rescale and self.secondary_series:
points = self._rescale(serie.points)
else:
points = serie.points
view_values = list(map(self.view, points))
if serie.show_dots:
for i, (x, y) in enumerate(view_values):
if None in (x, y):
continue
#modified
if self.logarithmic:
if points[i][1] == None or points[i][1] <= 0:
continue
if (serie.show_only_major_dots and
self.x_labels and i < len(self.x_labels) and
self.x_labels[i] not in self._x_labels_major):
continue
metadata = serie.metadata.get(i)
classes = []
if x > self.view.width / 2:
classes.append('left')
if y > self.view.height / 2:
classes.append('top')
classes = ' '.join(classes)
self._confidence_interval(
serie_node['overlay'], x, y, serie.values[i], metadata)
dots = decorate(
self.svg,
self.svg.node(serie_node['overlay'], class_="dots"),
metadata)
val = self._format(serie, i)
alter(self.svg.transposable_node(
dots, 'circle', cx=x, cy=y, r=serie.dots_size,
class_='dot reactive tooltip-trigger'), metadata)
self._tooltip_data(
dots, val, x, y,
xlabel=self._get_x_label(i))
self._static_value(
serie_node, val,
x + self.style.value_font_size,
y + self.style.value_font_size,
metadata)
if serie.stroke:
if self.interpolate:
points = serie.interpolated
if rescale and self.secondary_series:
points = self._rescale(points)
view_values = list(map(self.view, points))
if serie.fill:
view_values = self._fill(view_values)
if serie.allow_interruptions:
# view_values are in form [(x1, y1), (x2, y2)]. We
# need to split that into multiple sequences if a
# None is present here
sequences = []
cur_sequence = []
for x, y in view_values:
if y is None and len(cur_sequence) > 0:
# emit current subsequence
sequences.append(cur_sequence)
cur_sequence = []
elif y is None: # just discard
continue
else:
cur_sequence.append((x, y)) # append the element
if len(cur_sequence) > 0: # emit last possible sequence
sequences.append(cur_sequence)
else:
# plain vanilla rendering
sequences = [view_values]
#modified
if self.logarithmic:
for seq in sequences:
for ele in seq[::-1]:
if points[seq.index(ele)][1]==None or points[seq.index(ele)][1] <= 0:
del seq[seq.index(ele)]
for seq in sequences:
self.svg.line(
serie_node['plot'], seq, close=self._self_close,
class_='line reactive' +
(' nofill' if not serie.fill else ''))
def _compute(self):
"""Compute y min and max and y scale and set labels"""
# X Labels
if self.horizontal:
self._x_pos = [
x / (self._len - 1) for x in range(self._len)
][::-1] if self._len != 1 else [.5] # Center if only one value
else:
self._x_pos = [
x / (self._len - 1) for x in range(self._len)
] if self._len != 1 else [.5] # Center if only one value
self._points(self._x_pos)
if self.include_x_axis:
# Y Label
self._box.ymin = min(self._min or 0, 0)
self._box.ymax = max(self._max or 0, 0)
else:
self._box.ymin = self._min
self._box.ymax = self._max
def _plot(self):
"""Plot the serie lines and secondary serie lines"""
for serie in self.series:
self.line(serie)
for serie in self.secondary_series:
self.line(serie, True)

15
pygal/test/test_line_log_none_max_solved.py

@ -0,0 +1,15 @@
# This file is test file for NoneMaxSolved
# I have modified the line.py and passed other test
# This test is for us to test whether the none value
# in the Log graph will be max or not (issue #309)
from __future__ import division
from pygal import Line
from pygal.test.utils import texts
from math import cos, sin
chart = Line(title='test', x_label_rotation=90, human_readable=True, logarithmic=True)
chart.add('test 1', [None, None, None, None, -38, 48, None, 4422, 34443, 345586, 40003452, 1235533, 2235533])
chart.add('test 2', [ 1, 7, 19, 30, 40, 20, 38, 283, 2937, 29374, 20399, 293874, 3947])
q = line.render_pyquery()
assert len(q(".dots")) == 20

15
pygal/test/test_line_log_none_max_solved.py~

@ -0,0 +1,15 @@
# This file is test file for NoneMaxSolved
# I have modified the line.py and passed other test
# This test is for us to test whether the none value
# in the Log graph will be max or not (issue #309)
from __future__ import division
from pygal import Line
from pygal.test.utils import texts
from math import cos, sin
chart = Line(title='test', x_label_rotation=90, human_readable=True, logarithmic=True)
chart.add('test 1', [None, None, None, None, -38, 48, None, 4422, 34443, 345586, 40003452, 1235533, 2235533])
chart.add('test 2', [ 1, 7, 19, 30, 40, 20, 38, 283, 2937, 29374, 20399, 293874, 3947])
chart.render_to_file("NoneMaxSolved.svg")

2
pygal/view.py

@ -343,7 +343,7 @@ class LogView(View):
def y(self, y):
"""Project y"""
if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0:
return 0
return 0.00001
return (self.height - self.height *
(log10(y) - self.log10_ymin) / (
self.log10_ymax - self.log10_ymin))

423
pygal/view.py~

@ -0,0 +1,423 @@
# -*- 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.000001
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)
Loading…
Cancel
Save