source: qesdi/linplot/trunk/src/linplot/layout_manager.py @ 6446

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/qesdi/linplot/trunk/src/linplot/layout_manager.py@6446
Revision 6446, 8.9 KB checked in by pnorton, 11 years ago (diff)

Made several changes to try and improve the layout of the plot. Also introduced rotating axis labels to avoid labels being drawn on top of each other.

Line 
1'''
2Created on 29 Jan 2010
3
4@author: pnorton
5'''
6
7import math
8import logging
9import matplotlib
10
11log = logging.getLogger(__name__)
12
13from linplot.config import config
14
15
16#AXIS_POSITION = [0.1, 0.15, 0.7, 0.7]
17#LEGEND_POSITION = [0.82, 0.15, 0.15, 0.7]
18
19# one row
20BASE_AXIS_POSITION  =  [0.025, 0.2, 0.95, 0.775]
21BASE_LEGEND_POSITION = [0.025, 0.025, 0.95, 0.175]
22
23# two rows
24#AXIS_POSITION = [0.1, 0.25, 0.8, 0.65]
25#LEGEND_POSITION = [0.1, 0.05, 0.8, 0.1]
26
27##three rows
28#AXIS_POSITION = [0.1, 0.30, 0.8, 0.6]
29#LEGEND_POSITION = [0.1, 0.05, 0.8, 0.15]
30
31LEGEND_ITEM_WIDTH = 65
32LEGEND_ITEM_HEIGHT = 20
33LEGEND_VERTICAL_PADDING = 10
34
35class EmptyFigure(object):
36    pass
37
38class LayoutManager(object):
39    """
40    Responsible for laying out the axis of the plot + legend so that the text is
41    readable and the plot is as large as possible.
42    """
43   
44    def __init__(self, rendererClass, dpi, width, height):
45        self.pixelBorder = 14
46       
47        if rendererClass == matplotlib.backends.backend_agg.RendererAgg:
48            self._renderer= rendererClass(width, height, dpi)
49        else: 
50            self._renderer= rendererClass(dpi)
51       
52        self._dpi = dpi
53        self._width = width
54        self._height = height
55   
56    def calculateLegendNcol(self, labels):
57       
58        ncol = 1
59       
60        legendFont = config['LegendFont']
61       
62        legendText = [matplotlib.text.Text(0,0,l) for l in labels]
63        for lt in legendText:
64            # need to create a mock figure with a dpi property to get the
65            # text width
66            lt.figure = EmptyFigure()
67            lt.figure.dpi = self._dpi
68            legendFont.setProperties(lt)
69           
70        labelWidths = [self._getLabelWidth(l) + LEGEND_ITEM_WIDTH for l in legendText]
71       
72        legendWidth = (BASE_LEGEND_POSITION[2] - BASE_LEGEND_POSITION[0] ) * self._width
73        log.debug("labels = %s" % (labels,))
74        log.debug("labelWidths = %s, legendWidth = %s" % (labelWidths, legendWidth,))
75        # try to fit up to 4 columns in
76        for i in range(4, 1, -1):
77           
78            if max(labelWidths) * i < legendWidth and len(labels) >= i:
79                ncol = i
80                break
81       
82        log.debug("ncol = %s" % (ncol,))
83       
84        return ncol
85       
86    def setAxisPositions(self, labels, ncol, legend, legendAxes, plotAxes):
87       
88        # need to work out how tall the legend will be at the bottom of the
89        # figure.
90       
91        # work out the maximum height of all the legend texts
92        heights = [self._getLabelHeight(lbl) for lbl in legend.get_texts()]
93        maxHeight = max(heights)
94       
95        # the maximum height may be due to the legend box rather than the text
96        maxHeight = max([maxHeight, LEGEND_ITEM_HEIGHT])
97       
98        nlines = len(labels)
99        nrow = math.ceil(float(nlines)/ncol)       
100       
101        # get the height needed as a proportion of the plot
102        heightNeeded = ((maxHeight + LEGEND_VERTICAL_PADDING) * nrow) / float(self._height)
103
104        legendPos = AxisPosition.fromMatplotlibPosition(legendAxes.get_position())
105        legendPos.setHeight(heightNeeded)
106       
107        # set bottom of the plot to match the top of the legend
108        plotPos = AxisPosition.fromMatplotlibPosition(plotAxes.get_position())
109        plotPos.y0 = legendPos.y1
110               
111        #set the positions
112        plotAxes.set_position(plotPos.getLocationAndSize())
113        legendAxes.set_position(legendPos.getLocationAndSize())
114   
115    def fitPlotInAxis(self, axes):
116
117        self._setupAxisTicks(axes)
118
119        width, height = float(self._width), float(self._height)
120
121        self._rotateLabelsIfNeeded(axes, width, height)
122       
123        pad = self._calculateRequiredPadding(axes, width, height)
124               
125        p = AxisPosition.fromMatplotlibPosition(axes.get_position())
126   
127        p.x0 +=  pad.left
128        p.y0 +=  pad.bottom
129        p.x1 -=  pad.right
130        p.y1 -=  pad.top
131       
132        axes.set_position( p.getLocationAndSize())
133   
134    def _rotateLabelsIfNeeded(self, axes, width, height):
135        labelWidths = [self._getLabelWidth(l) for l in axes.get_xticklabels()]
136     
137        c = 0.9 
138        totWidth = (max(labelWidths) * len(labelWidths)) * c
139       
140        pos = AxisPosition.fromMatplotlibPosition(axes.get_position())
141        plotWidth = (pos.x1 - pos.x0) * width
142       
143        if totWidth > plotWidth:
144            log.debug("Rotating x axis labels")
145           
146            for l in axes.get_xticklabels():
147                # set the rotating about the right hand side of the text
148                l.set_rotation_mode('anchor')
149                l.set_va('center')
150                l.set_ha('right')
151                l.set_rotation(80)       
152   
153    def _calculateRequiredPadding(self, axes, width, height):
154        pad = Padding()
155        pad.updateAll(self.pixelBorder / height)
156       
157        # calculate the space needed for the title
158        titleHeight = self._getLabelHeight(axes.title)
159        pad.updateTop((titleHeight + self.pixelBorder) / height)
160
161        # calculate the space needed for the left axes label and title
162        maxYLabelWidth = max( [ self._getLabelWidth(l) for l in axes.get_yticklabels()])
163        yTitleWidth = self._getLabelWidth(axes.yaxis.label)
164        pad.updateLeft( (yTitleWidth + maxYLabelWidth + 2*self.pixelBorder) / width)
165       
166        # calculate the space needed for the bottom axis label and title
167        maxXLabelHeight = max( [ self._getLabelHeight(l) for l in axes.get_xticklabels()])
168        xTitleHeight = self._getLabelHeight(axes.xaxis.label)
169        pad.updateBottom((xTitleHeight + maxXLabelHeight + 2*self.pixelBorder) / height)
170       
171        rotated = axes.get_xticklabels()[0].get_rotation() > 0 
172        log.debug("rotated = %s" % (rotated,))
173       
174        if rotated:
175            #calculate the left width due to the first label
176            w = self._getLabelWidth(axes.get_xticklabels()[0])
177            pad.updateLeft( (w + self.pixelBorder) / width)
178       
179        if not rotated:
180            #calculate the right width due to the last label
181            w = self._getLabelWidth(axes.get_xticklabels()[-1])
182            pad.updateRight( ( (w/2.0) + self.pixelBorder) / width)
183       
184        return pad       
185
186    def _getLabelWidth(self, lbl):
187        extent = lbl.get_window_extent(renderer=self._renderer, dpi=self._dpi)
188        return extent.x1 - extent.x0
189   
190    def _getLabelHeight(self, lbl):
191        extent = lbl.get_window_extent(renderer=self._renderer, dpi=self._dpi)
192        return extent.y1 - extent.y0
193
194    def _setupAxisTicks(self, axes):
195        """
196        Because the ticks labels aren't usually calculated until they are draw
197        we need to manually trigger the updating of the positions and labels
198        """
199        for axis in [axes.xaxis, axes.yaxis]:
200            for tick, loc, label in axis.iter_ticks():
201                tick.update_position(loc)
202               
203                if tick.label1.get_text() == "":
204                    tick.set_label1(label)
205                   
206                if tick.label2.get_text() == "":
207                    tick.set_label2(label)
208       
209
210class AxisPosition(object):
211    def __init__(self, x0, y0, x1, y1):
212        self.x0 = x0
213        self.y0 = y0
214        self.x1 = x1
215        self.y1 = y1
216   
217    @staticmethod
218    def fromMatplotlibPosition(pos):
219        return AxisPosition(pos.x0, pos.y0, pos.x1, pos.y1)
220   
221    def setHeight(self, height):
222        """
223        Sets the value of y1 so that the position has the height given
224        """
225        self.y1 = self.y0 + height
226       
227    def setWidth(self, width):
228        """
229        Sets the value of x1 so the the position has the width given
230        """
231        self.x1 = self.x0 + width
232   
233    def getLocationAndSize(self):
234        return [self.x0, self.y0, self.x1 - self.x0, self.y1 - self.y0]
235   
236    def __str__(self):
237        return "AxisPosition: x0=%s, y0=%s, x1=%s, y1=%s" % (self.x0, self.y0, self.x1, self.y1)
238       
239
240class AxesSize(object):
241    def __init__(self, width, height):
242        self.width = width
243        self.height = height
244
245class Padding(object):
246   
247    def __init__(self, left=0.0, bottom=0.0, right=0.0, top=0.0):
248        self.left = left
249        self.bottom = bottom
250        self.right = right
251        self.top = top
252   
253    def updateAll(self, value):
254        self.updateLeft(value)
255        self.updateRight(value)
256        self.updateTop(value)
257        self.updateBottom(value)
258   
259    def updateLeft(self, newPadding):
260        self.left = max([self.left, newPadding])
261   
262    def updateRight(self, newPadding):
263        self.right = max([self.right, newPadding])
264   
265    def updateTop(self, newPadding):
266        self.top = max([self.top, newPadding])
267   
268    def updateBottom(self, newPadding):
269        self.bottom = max([self.bottom, newPadding])
270   
271    def __str__(self):
272        return "Padding: left=%s, bottom=%s, right=%s, top=%s" % (self.left, self.bottom, self.right, self.top)
273
Note: See TracBrowser for help on using the repository browser.