~ubuntu-branches/ubuntu/trusty/guiqwt/trusty

« back to all changes in this revision

Viewing changes to guiqwt/curve.py

  • Committer: Bazaar Package Importer
  • Author(s): Picca Frédéric-Emmanuel
  • Date: 2011-04-07 22:41:50 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20110407224150-kkhppnq7rp2c8b3c
Tags: 2.1.0-1
* Imported Upstream version 2.1.0
* debian/patches/
  - 0001-features-01_bts614083.patch (delete)
  - 0001-feature-fix-the-documentation-build.patch (new)
* add and install the sift desktop file
* recommends python-dicom (Closes: #621421)
  thanks Junqian Gordon Xu for the report

Show diffs side-by-side

added added

removed removed

Lines of Context:
103
103
from PyQt4.QtGui import (QMenu, QListWidget, QListWidgetItem, QVBoxLayout,
104
104
                         QToolBar, QMessageBox, QBrush)
105
105
from PyQt4.QtCore import Qt, QPoint, QPointF, QLineF, SIGNAL, QRectF, QLine
106
 
from PyQt4.Qwt5 import QwtPlotCurve, QwtPlotGrid, QwtPlotItem, QwtScaleMap
107
106
 
108
107
from guidata.utils import assert_interfaces_valid, update_dataset
109
108
from guidata.configtools import get_icon, get_image_layout
110
109
from guidata.qthelpers import create_action, add_actions
111
110
 
112
111
# Local imports
 
112
from guiqwt.transitional import (QwtPlotCurve, QwtPlotGrid, QwtPlotItem,
 
113
                                 QwtScaleMap)
113
114
from guiqwt.config import CONF, _
114
115
from guiqwt.interfaces import (IBasePlotItem, IDecoratorItemType,
115
116
                               ISerializableType, ICurveItemType,
116
117
                               ITrackableItemType, IPanel)
117
118
from guiqwt.panels import PanelWidget, ID_ITEMLIST
118
 
from guiqwt.baseplot import EnhancedQwtPlot
 
119
from guiqwt.baseplot import BasePlot
119
120
from guiqwt.styles import GridParam, CurveParam, ErrorBarParam, SymbolParam
120
121
from guiqwt.shapes import Marker
121
122
from guiqwt.signals import (SIG_ACTIVE_ITEM_CHANGED, SIG_ITEMS_CHANGED,
122
 
                            SIG_ITEM_REMOVED, SIG_AXIS_DIRECTION_CHANGED)
 
123
                            SIG_ITEM_REMOVED, SIG_AXIS_DIRECTION_CHANGED,
 
124
                            SIG_PLOT_AXIS_CHANGED)
123
125
 
124
126
 
125
127
def seg_dist(P, P0, P1):
159
161
    V[:, 0] = X1-X0
160
162
    V[:, 1] = Y1-Y0
161
163
    dP = np.array(P).reshape(1, 2) - PP
162
 
    nV = np.sqrt(norm2(V))
 
164
    nV = np.sqrt(norm2(V)).clip(1e-12) # clip: avoid division by zero
163
165
    w2 = V/nV[:, np.newaxis]
164
166
    w = np.array([ -w2[:,1], w2[:,0] ]).T
165
167
    distances = np.fabs((dP*w).sum(axis=1))
192
194
    __implements__ = (IBasePlotItem,)
193
195
    
194
196
    _readonly = True
 
197
    _private = False
195
198
    
196
199
    def __init__(self, gridparam=None):
197
200
        super(GridItem, self).__init__()
219
222
    def is_readonly(self):
220
223
        """Return object read-only state"""
221
224
        return self._readonly
 
225
        
 
226
    def set_private(self, state):
 
227
        """Set object as private"""
 
228
        self._private = state
 
229
        
 
230
    def is_private(self):
 
231
        """Return True if object is private"""
 
232
        return self._private
222
233
 
223
234
    def can_select(self):
224
235
        return False
240
251
    def hit_test(self, pos):
241
252
        return sys.maxint, 0, False, None
242
253
 
243
 
    def move_local_point_to(self, handle, pos ):
 
254
    def move_local_point_to(self, handle, pos, ctrl=None):
244
255
        pass
245
256
 
246
257
    def move_local_shape(self, old_pos, new_pos):
247
258
        pass
248
259
        
249
 
    def move_with_selection(self, dx, dy):
 
260
    def move_with_selection(self, delta_x, delta_y):
250
261
        pass
251
262
 
252
263
    def update_params(self):
270
281
    __implements__ = (IBasePlotItem,)
271
282
    
272
283
    _readonly = False
 
284
    _private = False
273
285
    
274
286
    def __init__(self, curveparam=None):
275
287
        super(CurveItem, self).__init__()
314
326
    def is_readonly(self):
315
327
        """Return object readonly state"""
316
328
        return self._readonly
 
329
        
 
330
    def set_private(self, state):
 
331
        """Set object as private"""
 
332
        self._private = state
 
333
        
 
334
    def is_private(self):
 
335
        """Return True if object is private"""
 
336
        return self._private
317
337
 
318
338
    def invalidate_plot(self):
319
339
        plot = self.plot()
419
439
        ay = self.yAxis()
420
440
        return plot.invTransform(ax, pos.x()), plot.invTransform(ay, pos.y())
421
441
 
422
 
    def move_local_point_to(self, handle, pos):
 
442
    def move_local_point_to(self, handle, pos, ctrl=None):
423
443
        if self.immutable:
424
444
            return
425
445
        if handle < 0 or handle > self._x.shape[0]:
439
459
        self._y += (ny-oy)
440
460
        self.setData(self._x, self._y)
441
461
        
442
 
    def move_with_selection(self, dx, dy):
 
462
    def move_with_selection(self, delta_x, delta_y):
443
463
        """
444
464
        Translate the shape together with other selected items
445
 
        dx, dy: translation in plot coordinates
 
465
        delta_x, delta_y: translation in plot coordinates
446
466
        """
447
 
        self._x += dx
448
 
        self._y += dy
 
467
        self._x += delta_x
 
468
        self._y += delta_y
449
469
        self.setData(self._x, self._y)
450
470
 
451
471
    def update_params(self):
499
519
        """
500
520
        return self._x, self._y, self._dx, self._dy
501
521
 
502
 
    def set_data(self, x, y, dx, dy):
 
522
    def set_data(self, x, y, dx=None, dy=None):
503
523
        """
504
524
        Set error-bar curve data:
505
525
            * x: NumPy array
563
583
    def boundingRect(self):
564
584
        """Return the bounding rectangle of the data, error bars included"""
565
585
        xmin, xmax, ymin, ymax = self.get_minmax_arrays()
 
586
        if xmin is None or xmin.size == 0:
 
587
            return super(ErrorBarCurveItem, self).boundingRect()
566
588
        return QRectF( xmin.min(), ymin.min(),
567
589
                       xmax.max()-xmin.min(), ymax.max()-ymin.min() )
568
590
        
569
591
    def draw(self, painter, xMap, yMap, canvasRect):
570
592
        x = self._x
 
593
        if x is None:
 
594
            return
571
595
        y = self._y
572
596
        tx = vmap(xMap, x)
573
597
        ty = vmap(yMap, y)
749
773
                                        AnnotatedPoint, AnnotatedSegment)
750
774
        from guiqwt.shapes import (SegmentShape, RectangleShape, EllipseShape,
751
775
                                   PointShape, PolygonShape, Axes,
752
 
                                   XRangeSelection)
 
776
                                   XRangeSelection, VerticalCursor,
 
777
                                   HorizontalCursor)
753
778
        from guiqwt.image import (BaseImageItem, Histogram2DItem,
754
779
                                  ImageFilterItem)
755
780
        from guiqwt.histogram import HistogramItem
774
799
                            (Axes, 'gtaxes.png'),
775
800
                            (Marker, 'marker.png'),
776
801
                            (XRangeSelection, 'xrange.png'),
 
802
                            (VerticalCursor, 'vcursor.png'),
 
803
                            (HorizontalCursor, 'hcursor.png'),
777
804
                            (PolygonShape, 'freeform.png'),
778
805
                            (Histogram2DItem, 'histogram2d.png'),
779
806
                            (ImageFilterItem, 'funct.png'),
791
818
        self.plot = plot
792
819
        _block = self.blockSignals(True)
793
820
        active = plot.get_active_item()
794
 
        self.items = plot.get_items(z_sorted=True)
 
821
        self.items = plot.get_public_items(z_sorted=True)
795
822
        self.clear()
796
823
        for item in self.items:
797
824
            title = item.title().text()
861
888
            self.plot.del_items(items)
862
889
            self.plot.replot()
863
890
            for item in items:
864
 
                self.parent().emit(SIG_ITEM_REMOVED, item)
 
891
                self.plot.emit(SIG_ITEM_REMOVED, item)
865
892
        
866
893
 
867
894
class PlotItemList(PanelWidget):
868
895
    """Construct the `plot item list panel`"""
869
896
    __implements__ = (IPanel,)
870
897
    PANEL_ID = ID_ITEMLIST
 
898
    PANEL_TITLE = _("Item list")
 
899
    PANEL_ICON = "item_list.png"
871
900
    
872
901
    def __init__(self, parent):
873
902
        super(PlotItemList, self).__init__(parent)
874
 
        widget_title = _("Item list")
875
 
        widget_icon = "item_list.png"
876
903
        self.manager = None
877
904
        
878
905
        vlayout = QVBoxLayout()
879
906
        self.setLayout(vlayout)
880
907
        
881
908
        style = "<span style=\'color: #444444\'><b>%s</b></span>"
882
 
        layout, _label = get_image_layout(widget_icon, style % widget_title,
 
909
        layout, _label = get_image_layout(self.PANEL_ICON,
 
910
                                          style % self.PANEL_TITLE,
883
911
                                          alignment=Qt.AlignCenter)
884
912
        vlayout.addLayout(layout)
885
913
        self.listwidget = ItemListWidget(self)
888
916
        toolbar = QToolBar(self)
889
917
        vlayout.addWidget(toolbar)
890
918
        add_actions(toolbar, self.listwidget.menu_actions)
891
 
        
892
 
        self.setWindowIcon(get_icon(widget_icon))
893
 
        self.setWindowTitle(widget_title)
894
919
 
895
920
    def register_panel(self, manager):
896
921
        """Register panel to plot manager"""
897
922
        self.manager = manager
898
923
        self.listwidget.register_panel(manager)
 
924
                         
 
925
    def configure_panel(self):
 
926
        """Configure panel"""
 
927
        pass
899
928
 
900
929
assert_interfaces_valid(PlotItemList)
901
930
 
902
931
 
903
 
class CurvePlot(EnhancedQwtPlot):
 
932
class CurvePlot(BasePlot):
904
933
    """
905
934
    Construct a 2D curve plotting widget 
906
 
    (this class inherits :py:class:`guiqwt.baseplot.EnhancedQwtPlot`)
 
935
    (this class inherits :py:class:`guiqwt.baseplot.BasePlot`)
907
936
        * parent: parent widget
908
937
        * title: plot title
909
938
        * xlabel: (bottom axis title, top axis title) or bottom axis title only
910
939
        * ylabel: (left axis title, right axis title) or left axis title only
911
940
        * gridparam: GridParam instance
 
941
        * axes_synchronised: keep all x and y axes synchronised when zomming or
 
942
                             panning
912
943
    """
913
944
    AUTOSCALE_TYPES = (CurveItem,)
914
945
    def __init__(self, parent=None, title=None, xlabel=None, ylabel=None,
915
 
                 gridparam=None, section="plot"):
 
946
                 gridparam=None, section="plot", axes_synchronised=False):
916
947
        super(CurvePlot, self).__init__(parent, section)
917
948
 
918
949
        self.axes_reverse = [False]*4
923
954
 
924
955
        self.set_antialiasing(CONF.get(section, "antialiasing"))
925
956
        
 
957
        self.axes_synchronised = axes_synchronised
 
958
        
926
959
        # Installing our own event filter:
927
960
        # (PyQwt's event filter does not fit our needs)
928
961
        self.canvas().installEventFilter(self.filter)
1011
1044
            self.curve_marker.setVisible(False)
1012
1045
            if vis_cross or vis_curve:
1013
1046
                self.replot()
 
1047
                
 
1048
    def get_axes_to_update(self, dx, dy):
 
1049
        if self.axes_synchronised:
 
1050
            axes = []
 
1051
            for axis_name in self.AXIS_NAMES:
 
1052
                if axis_name in ("left", "right"):
 
1053
                    d = dy
 
1054
                else:
 
1055
                    d = dx
 
1056
                axes.append((d, self.get_axis_id(axis_name)))
 
1057
            return axes
 
1058
        else:
 
1059
            xaxis, yaxis = self.get_active_axes()
 
1060
            return [(dx, xaxis), (dy, yaxis)]
1014
1061
        
1015
1062
    def do_pan_view(self, dx, dy):
1016
1063
        """
1019
1066
        """
1020
1067
        auto = self.autoReplot()
1021
1068
        self.setAutoReplot(False)
1022
 
        xaxis, yaxis = self.get_active_axes()
1023
 
        active_axes = [ (dx, xaxis),
1024
 
                        (dy, yaxis) ]
1025
 
        for (x1, x0, _, w), k in active_axes:
 
1069
        axes_to_update = self.get_axes_to_update(dx, dy)
 
1070
        
 
1071
        for (x1, x0, _, w), k in axes_to_update:
1026
1072
            axis = self.axisScaleDiv(k)
1027
1073
            # pour les axes logs on bouge lbound et hbound relativement
1028
1074
            # à l'inverse du delta aux bords de l'axe
1034
1080
            self.setAxisScale(k, lbound-pos0, hbound-pos1)
1035
1081
        self.setAutoReplot(auto)
1036
1082
        self.replot()
 
1083
        # the signal MUST be emitted after replot, otherwise
 
1084
        # we receiver won't see the new bounds (don't know why?)
 
1085
        self.emit(SIG_PLOT_AXIS_CHANGED, self)
1037
1086
 
1038
1087
    def do_zoom_view(self, dx, dy, lock_aspect_ratio=False):
1039
1088
        """
1048
1097
        if lock_aspect_ratio:
1049
1098
            sens, x1, x0, s, w = dx
1050
1099
            F = 1+3*sens*float(x1-x0)/w
1051
 
        xaxis, yaxis = self.get_active_axes()
1052
 
        active_axes = [ (dx, xaxis),
1053
 
                        (dy, yaxis) ]
1054
 
        for (sens, x1, x0, s, w), k in active_axes:
 
1100
        axes_to_update = self.get_axes_to_update(dx, dy)
 
1101
                                    
 
1102
        for (sens, x1, x0, s, w), k in axes_to_update:
1055
1103
            axis = self.axisScaleDiv(k)
1056
1104
            lbound = axis.lowerBound()
1057
1105
            hbound = axis.upperBound()
1065
1113
            self.setAxisScale(k, l_new, l_new + F*rng)
1066
1114
        self.setAutoReplot(auto)
1067
1115
        self.replot()
 
1116
        # the signal MUST be emitted after replot, otherwise
 
1117
        # we receiver won't see the new bounds (don't know why?)
 
1118
        self.emit(SIG_PLOT_AXIS_CHANGED, self)
1068
1119
        
1069
1120
    def do_zoom_rect_view(self, start, end):
 
1121
        # XXX implement the case when axes are synchronised
1070
1122
        x1, y1 = start.x(), start.y()
1071
1123
        x2, y2 = end.x(), end.y()
1072
1124
        xaxis, yaxis = self.get_active_axes()
1084
1136
            self.setAxisScale(k, o1, o2)
1085
1137
        self.replot()
1086
1138
 
1087
 
    #---- EnhancedQwtPlot API --------------------------------------------------
1088
 
    def get_axis_title(self, axis):
1089
 
        """
1090
 
        Reimplement EnhancedQwtPlot method
1091
 
        
1092
 
        Return axis title
1093
 
            * axis: 'bottom', 'left', 'top' or 'right'
1094
 
        """
1095
 
        if axis in self.AXES:
1096
 
            axis = self.AXES[axis]
1097
 
        return super(CurvePlot, self).get_axis_title(axis)
1098
 
        
1099
 
    def set_axis_title(self, axis, title):
1100
 
        """
1101
 
        Reimplement EnhancedQwtPlot method
1102
 
        
1103
 
        Set axis title
1104
 
            * axis: 'bottom', 'left', 'top' or 'right'
1105
 
            * title: string
1106
 
        """
1107
 
        if axis in self.AXES:
1108
 
            axis = self.AXES[axis]
1109
 
        super(CurvePlot, self).set_axis_title(axis, title)
1110
 
 
1111
 
    def set_axis_font(self, axis, font):
1112
 
        """
1113
 
        Reimplement EnhancedQwtPlot method
1114
 
        
1115
 
        Set axis font
1116
 
            * axis: 'bottom', 'left', 'top' or 'right'
1117
 
            * font: QFont instance
1118
 
        """
1119
 
        if axis in self.AXES:
1120
 
            axis = self.AXES[axis]
1121
 
        super(CurvePlot, self).set_axis_font(axis, font)
1122
 
    
1123
 
    def set_axis_color(self, axis, color):
1124
 
        """
1125
 
        Reimplement EnhancedQwtPlot method
1126
 
        
1127
 
        Set axis color
1128
 
            * axis: 'bottom', 'left', 'top' or 'right'
1129
 
            * color: color name (string) or QColor instance
1130
 
        """
1131
 
        if axis in self.AXES:
1132
 
            axis = self.AXES[axis]
1133
 
        super(CurvePlot, self).set_axis_color(axis, color)
1134
 
 
 
1139
    #---- BasePlot API ---------------------------------------------------------
1135
1140
    def add_item(self, item, z=None):
1136
1141
        """
1137
1142
        Add a *plot item* instance to this *plot widget*
1174
1179
    
1175
1180
    def do_autoscale(self, replot=True):
1176
1181
        """Do autoscale on all axes"""
1177
 
        rect = None
1178
 
        for item in self.get_items():
1179
 
            if isinstance(item, self.AUTOSCALE_TYPES) and not item.is_empty() \
1180
 
               and item.isVisible():
1181
 
                bounds = item.boundingRect()
1182
 
                if rect is None:
1183
 
                    rect = bounds
1184
 
                else:
1185
 
                    rect = rect.united(bounds)
1186
 
        if rect is not None:
1187
 
            x0, x1 = rect.left(), rect.right()
1188
 
            y0, y1 = rect.top(), rect.bottom()
1189
 
            if x0 == x1: # same behavior as MATLAB
1190
 
                x0 -= 1
1191
 
                x1 += 1
1192
 
            if y0 == y1: # same behavior as MATLAB
1193
 
                y0 -= 1
1194
 
                y1 += 1
1195
 
            self.set_plot_limits(x0, x1, y0, y1)
1196
 
            if replot:
1197
 
                self.replot()
 
1182
        # XXX implement the case when axes are synchronised
 
1183
        for axis_id in self.AXIS_IDS:
 
1184
            vmin, vmax = None, None
 
1185
            if not self.axisEnabled(axis_id):
 
1186
                continue
 
1187
            for item in self.get_items():
 
1188
                if isinstance(item, self.AUTOSCALE_TYPES) \
 
1189
                   and not item.is_empty() and item.isVisible():
 
1190
                    bounds = item.boundingRect()
 
1191
                    if axis_id == item.xAxis():
 
1192
                        xmin, xmax = bounds.left(), bounds.right()
 
1193
                        if vmin is None or xmin < vmin:
 
1194
                            vmin = xmin
 
1195
                        if vmax is None or xmax > vmax:
 
1196
                            vmax = xmax
 
1197
                    elif axis_id == item.yAxis():
 
1198
                        ymin, ymax = bounds.top(), bounds.bottom()
 
1199
                        if vmin is None or ymin < vmin:
 
1200
                            vmin = ymin
 
1201
                        if vmax is None or ymax > vmax:
 
1202
                            vmax = ymax
 
1203
            if vmin is None or vmax is None:
 
1204
                continue
 
1205
            if vmin == vmax: # same behavior as MATLAB
 
1206
                vmin -= 1
 
1207
                vmax += 1
 
1208
            else:
 
1209
                dv = vmax-vmin
 
1210
                vmin -= .002*dv
 
1211
                vmax += .002*dv
 
1212
            self.set_axis_limits(axis_id, vmin, vmax)
 
1213
        if replot:
 
1214
            self.replot()
 
1215
 
 
1216
    def set_axis_limits(self, axis_id, vmin, vmax):
 
1217
        """Set axis limits (minimum and maximum values)"""
 
1218
        axis_id = self.get_axis_id(axis_id)
 
1219
        vmin, vmax = sorted([vmin, vmax])
 
1220
        dv = vmax-vmin
 
1221
        if self.get_axis_direction(axis_id):
 
1222
            self.setAxisScale(axis_id, vmin+dv, vmin)
 
1223
        else:
 
1224
            self.setAxisScale(axis_id, vmin, vmin+dv)
1198
1225
            
1199
1226
    #---- Public API -----------------------------------------------------------    
1200
 
    def get_axis_direction(self, axis):
 
1227
    def get_axis_direction(self, axis_id):
1201
1228
        """
1202
1229
        Return axis direction of increasing values
1203
 
            * axis: axis id (QwtPlot.yLeft, QwtPlot.xBottom, ...)
 
1230
            * axis_id: axis id (BasePlot.Y_LEFT, BasePlot.X_BOTTOM, ...)
1204
1231
              or string: 'bottom', 'left', 'top' or 'right'
1205
1232
        """
1206
 
        axis_id = self.AXES.get(axis, axis)
 
1233
        axis_id = self.get_axis_id(axis_id)
1207
1234
        return self.axes_reverse[axis_id]
1208
1235
            
1209
 
    def set_axis_direction(self, axis, reverse=False):
 
1236
    def set_axis_direction(self, axis_id, reverse=False):
1210
1237
        """
1211
1238
        Set axis direction of increasing values
1212
 
            * axis: axis id (QwtPlot.yLeft, QwtPlot.xBottom, ...)
 
1239
            * axis_id: axis id (BasePlot.Y_LEFT, BasePlot.X_BOTTOM, ...)
1213
1240
              or string: 'bottom', 'left', 'top' or 'right'
1214
1241
            * reverse: False (default)
1215
1242
                - x-axis values increase from left to right
1218
1245
                - x-axis values increase from right to left
1219
1246
                - y-axis values increase from top to bottom
1220
1247
        """
1221
 
        axis_id = self.AXES.get(axis, axis)
 
1248
        axis_id = self.get_axis_id(axis_id)
1222
1249
        if reverse != self.axes_reverse[axis_id]:
1223
1250
            self.replot()
1224
1251
            self.axes_reverse[axis_id] = reverse
1274
1301
                curve.setRenderHint(QwtPlotItem.RenderAntialiased,
1275
1302
                                    self.antialiased)
1276
1303
 
1277
 
    def set_plot_limits(self, x0, x1, y0, y1):
 
1304
    def set_plot_limits(self, x0, x1, y0, y1, xaxis="bottom", yaxis="left"):
1278
1305
        """Set plot scale limits"""
1279
 
        dy = y1-y0
1280
 
        if self.get_axis_direction(self.yLeft):
1281
 
            self.setAxisScale(self.yLeft, y0+dy, y0)
1282
 
        else:
1283
 
            self.setAxisScale(self.yLeft, y0, y0+dy)
1284
 
        dx = x1-x0
1285
 
        if self.get_axis_direction(self.xBottom):
1286
 
            self.setAxisScale(self.xBottom, x0+dx, x0)
1287
 
        else:
1288
 
            self.setAxisScale(self.xBottom, x0, x0+dx)
 
1306
        self.set_axis_limits(yaxis, y0, y1)
 
1307
        self.set_axis_limits(xaxis, x0, x1)     
1289
1308
        self.updateAxes()
1290
 
        self.emit(SIG_AXIS_DIRECTION_CHANGED, self, self.yLeft)
1291
 
        self.emit(SIG_AXIS_DIRECTION_CHANGED, self, self.xBottom)
 
1309
        self.emit(SIG_AXIS_DIRECTION_CHANGED, self, self.get_axis_id(yaxis))
 
1310
        self.emit(SIG_AXIS_DIRECTION_CHANGED, self, self.get_axis_id(xaxis))
 
1311
        
 
1312
    def set_plot_limits_synchronised(self, x0, x1, y0, y1):
 
1313
        for yaxis, xaxis in (("left", "bottom"), ("right", "top")):
 
1314
            self.set_plot_limits(x0, x1, y0, y1, xaxis=xaxis, yaxis=yaxis)
 
1315
        
 
1316
    def get_plot_limits(self, xaxis="bottom", yaxis="left"):
 
1317
        """Return plot scale limits"""
 
1318
        x0, x1 = self.get_axis_limits(xaxis)
 
1319
        y0, y1 = self.get_axis_limits(yaxis)
 
1320
        return x0, x1, y0, y1
1292
1321
        
 
 
b'\\ No newline at end of file'