mirror of https://github.com/Kozea/pygal.git
qins
8 years ago
6 changed files with 682 additions and 1 deletions
@ -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) |
@ -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 |
@ -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") |
@ -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…
Reference in new issue