Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / chart / axis / Axis.js
1 /**
2  * @class Ext.chart.axis.Axis
3  * @extends Ext.chart.axis.Abstract
4  * 
5  * Defines axis for charts. The axis position, type, style can be configured.
6  * The axes are defined in an axes array of configuration objects where the type, 
7  * field, grid and other configuration options can be set. To know more about how 
8  * to create a Chart please check the Chart class documentation. Here's an example for the axes part:
9  * An example of axis for a series (in this case for an area chart that has multiple layers of yFields) could be:
10  * 
11   <pre><code>
12     axes: [{
13         type: 'Numeric',
14         grid: true,
15         position: 'left',
16         fields: ['data1', 'data2', 'data3'],
17         title: 'Number of Hits',
18         grid: {
19             odd: {
20                 opacity: 1,
21                 fill: '#ddd',
22                 stroke: '#bbb',
23                 'stroke-width': 1
24             }
25         },
26         minimum: 0
27     }, {
28         type: 'Category',
29         position: 'bottom',
30         fields: ['name'],
31         title: 'Month of the Year',
32         grid: true,
33         label: {
34             rotate: {
35                 degrees: 315
36             }
37         }
38     }]
39    </code></pre>
40  * 
41  * In this case we use a `Numeric` axis for displaying the values of the Area series and a `Category` axis for displaying the names of
42  * the store elements. The numeric axis is placed on the left of the screen, while the category axis is placed at the bottom of the chart. 
43  * Both the category and numeric axes have `grid` set, which means that horizontal and vertical lines will cover the chart background. In the 
44  * category axis the labels will be rotated so they can fit the space better.
45  */
46 Ext.define('Ext.chart.axis.Axis', {
47
48     /* Begin Definitions */
49
50     extend: 'Ext.chart.axis.Abstract',
51
52     alternateClassName: 'Ext.chart.Axis',
53
54     requires: ['Ext.draw.Draw'],
55
56     /* End Definitions */
57
58     /**
59      * @cfg {Number} majorTickSteps 
60      * If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value.
61      */
62
63     /**
64      * @cfg {Number} minorTickSteps 
65      * The number of small ticks between two major ticks. Default is zero.
66      */
67
68     /**
69      * @cfg {Number} dashSize 
70      * The size of the dash marker. Default's 3.
71      */
72     dashSize: 3,
73     
74     /**
75      * @cfg {String} position
76      * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`.
77      */
78     position: 'bottom',
79     
80     // @private
81     skipFirst: false,
82     
83     /**
84      * @cfg {Number} length
85      * Offset axis position. Default's 0.
86      */
87     length: 0,
88     
89     /**
90      * @cfg {Number} width
91      * Offset axis width. Default's 0.
92      */
93     width: 0,
94     
95     majorTickSteps: false,
96
97     // @private
98     applyData: Ext.emptyFn,
99
100     // @private creates a structure with start, end and step points.
101     calcEnds: function() {
102         var me = this,
103             math = Math,
104             mmax = math.max,
105             mmin = math.min,
106             store = me.chart.substore || me.chart.store,
107             series = me.chart.series.items,
108             fields = me.fields,
109             ln = fields.length,
110             min = isNaN(me.minimum) ? Infinity : me.minimum,
111             max = isNaN(me.maximum) ? -Infinity : me.maximum,
112             prevMin = me.prevMin,
113             prevMax = me.prevMax,
114             aggregate = false,
115             total = 0,
116             excludes = [],
117             outfrom, outto,
118             i, l, values, rec, out;
119
120         //if one series is stacked I have to aggregate the values
121         //for the scale.
122         for (i = 0, l = series.length; !aggregate && i < l; i++) {
123             aggregate = aggregate || series[i].stacked;
124             excludes = series[i].__excludes || excludes;
125         }
126         store.each(function(record) {
127             if (aggregate) {
128                 if (!isFinite(min)) {
129                     min = 0;
130                 }
131                 for (values = [0, 0], i = 0; i < ln; i++) {
132                     if (excludes[i]) {
133                         continue;
134                     }
135                     rec = record.get(fields[i]);
136                     values[+(rec > 0)] += math.abs(rec);
137                 }
138                 max = mmax(max, -values[0], values[1]);
139                 min = mmin(min, -values[0], values[1]);
140             }
141             else {
142                 for (i = 0; i < ln; i++) {
143                     if (excludes[i]) {
144                         continue;
145                     }
146                     value = record.get(fields[i]);
147                     max = mmax(max, value);
148                     min = mmin(min, value);
149                 }
150             }
151         });
152         if (!isFinite(max)) {
153             max = me.prevMax || 0;
154         }
155         if (!isFinite(min)) {
156             min = me.prevMin || 0;
157         }
158         //normalize min max for snapEnds.
159         if (min != max && (max != (max >> 0))) {
160             max = (max >> 0) + 1;
161         }
162         out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ?  (me.majorTickSteps +1) : me.steps);
163         outfrom = out.from;
164         outto = out.to;
165         if (!isNaN(me.maximum)) {
166             //TODO(nico) users are responsible for their own minimum/maximum values set.
167             //Clipping should be added to remove lines in the chart which are below the axis.
168             out.to = me.maximum;
169         }
170         if (!isNaN(me.minimum)) {
171             //TODO(nico) users are responsible for their own minimum/maximum values set.
172             //Clipping should be added to remove lines in the chart which are below the axis.
173             out.from = me.minimum;
174         }
175         
176         //Adjust after adjusting minimum and maximum
177         out.step = (out.to - out.from) / (outto - outfrom) * out.step;
178         
179         if (me.adjustMaximumByMajorUnit) {
180             out.to += out.step;
181         }
182         if (me.adjustMinimumByMajorUnit) {
183             out.from -= out.step;
184         }
185         me.prevMin = min == max? 0 : min;
186         me.prevMax = max;
187         return out;
188     },
189
190     /**
191      * Renders the axis into the screen and updates it's position.
192      */
193     drawAxis: function (init) {
194         var me = this,
195             i, j,
196             x = me.x,
197             y = me.y,
198             gutterX = me.chart.maxGutter[0],
199             gutterY = me.chart.maxGutter[1],
200             dashSize = me.dashSize,
201             subDashesX = me.minorTickSteps || 0,
202             subDashesY = me.minorTickSteps || 0,
203             length = me.length,
204             position = me.position,
205             inflections = [],
206             calcLabels = false,
207             stepCalcs = me.applyData(),
208             step = stepCalcs.step,
209             steps = stepCalcs.steps,
210             from = stepCalcs.from,
211             to = stepCalcs.to,
212             trueLength,
213             currentX,
214             currentY,
215             path,
216             prev,
217             dashesX,
218             dashesY,
219             delta;
220         
221         //If no steps are specified
222         //then don't draw the axis. This generally happens
223         //when an empty store.
224         if (me.hidden || isNaN(step) || (from == to)) {
225             return;
226         }
227
228         me.from = stepCalcs.from;
229         me.to = stepCalcs.to;
230         if (position == 'left' || position == 'right') {
231             currentX = Math.floor(x) + 0.5;
232             path = ["M", currentX, y, "l", 0, -length];
233             trueLength = length - (gutterY * 2);
234         }
235         else {
236             currentY = Math.floor(y) + 0.5;
237             path = ["M", x, currentY, "l", length, 0];
238             trueLength = length - (gutterX * 2);
239         }
240         
241         delta = trueLength / (steps || 1);
242         dashesX = Math.max(subDashesX +1, 0);
243         dashesY = Math.max(subDashesY +1, 0);
244         if (me.type == 'Numeric') {
245             calcLabels = true;
246             me.labels = [stepCalcs.from];
247         }
248         if (position == 'right' || position == 'left') {
249             currentY = y - gutterY;
250             currentX = x - ((position == 'left') * dashSize * 2);
251             while (currentY >= y - gutterY - trueLength) {
252                 path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashSize * 2 + 1, 0);
253                 if (currentY != y - gutterY) {
254                     for (i = 1; i < dashesY; i++) {
255                         path.push("M", currentX + dashSize, Math.floor(currentY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
256                     }
257                 }
258                 inflections.push([ Math.floor(x), Math.floor(currentY) ]);
259                 currentY -= delta;
260                 if (calcLabels) {
261                     me.labels.push(me.labels[me.labels.length -1] + step);
262                 }
263                 if (delta === 0) {
264                     break;
265                 }
266             }
267             if (Math.round(currentY + delta - (y - gutterY - trueLength))) {
268                 path.push("M", currentX, Math.floor(y - length + gutterY) + 0.5, "l", dashSize * 2 + 1, 0);
269                 for (i = 1; i < dashesY; i++) {
270                     path.push("M", currentX + dashSize, Math.floor(y - length + gutterY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
271                 }
272                 inflections.push([ Math.floor(x), Math.floor(currentY) ]);
273                 if (calcLabels) {
274                     me.labels.push(me.labels[me.labels.length -1] + step);
275                 }
276             }
277         } else {
278             currentX = x + gutterX;
279             currentY = y - ((position == 'top') * dashSize * 2);
280             while (currentX <= x + gutterX + trueLength) {
281                 path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
282                 if (currentX != x + gutterX) {
283                     for (i = 1; i < dashesX; i++) {
284                         path.push("M", Math.floor(currentX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
285                     }
286                 }
287                 inflections.push([ Math.floor(currentX), Math.floor(y) ]);
288                 currentX += delta;
289                 if (calcLabels) {
290                     me.labels.push(me.labels[me.labels.length -1] + step);
291                 }
292                 if (delta === 0) {
293                     break;
294                 }
295             }
296             if (Math.round(currentX - delta - (x + gutterX + trueLength))) {
297                 path.push("M", Math.floor(x + length - gutterX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
298                 for (i = 1; i < dashesX; i++) {
299                     path.push("M", Math.floor(x + length - gutterX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
300                 }
301                 inflections.push([ Math.floor(currentX), Math.floor(y) ]);
302                 if (calcLabels) {
303                     me.labels.push(me.labels[me.labels.length -1] + step);
304                 }
305             }
306         }
307         if (!me.axis) {
308             me.axis = me.chart.surface.add(Ext.apply({
309                 type: 'path',
310                 path: path
311             }, me.axisStyle));
312         }
313         me.axis.setAttributes({
314             path: path
315         }, true);
316         me.inflections = inflections;
317         if (!init && me.grid) {
318             me.drawGrid();
319         }
320         me.axisBBox = me.axis.getBBox();
321         me.drawLabel();
322     },
323
324     /**
325      * Renders an horizontal and/or vertical grid into the Surface.
326      */
327     drawGrid: function() {
328         var me = this,
329             surface = me.chart.surface, 
330             grid = me.grid,
331             odd = grid.odd,
332             even = grid.even,
333             inflections = me.inflections,
334             ln = inflections.length - ((odd || even)? 0 : 1),
335             position = me.position,
336             gutter = me.chart.maxGutter,
337             width = me.width - 2,
338             vert = false,
339             point, prevPoint,
340             i = 1,
341             path = [], styles, lineWidth, dlineWidth,
342             oddPath = [], evenPath = [];
343         
344         if ((gutter[1] !== 0 && (position == 'left' || position == 'right')) ||
345             (gutter[0] !== 0 && (position == 'top' || position == 'bottom'))) {
346             i = 0;
347             ln++;
348         }
349         for (; i < ln; i++) {
350             point = inflections[i];
351             prevPoint = inflections[i - 1];
352             if (odd || even) {
353                 path = (i % 2)? oddPath : evenPath;
354                 styles = ((i % 2)? odd : even) || {};
355                 lineWidth = (styles.lineWidth || styles['stroke-width'] || 0) / 2;
356                 dlineWidth = 2 * lineWidth;
357                 if (position == 'left') {
358                     path.push("M", prevPoint[0] + 1 + lineWidth, prevPoint[1] + 0.5 - lineWidth, 
359                               "L", prevPoint[0] + 1 + width - lineWidth, prevPoint[1] + 0.5 - lineWidth,
360                               "L", point[0] + 1 + width - lineWidth, point[1] + 0.5 + lineWidth,
361                               "L", point[0] + 1 + lineWidth, point[1] + 0.5 + lineWidth, "Z");
362                 }
363                 else if (position == 'right') {
364                     path.push("M", prevPoint[0] - lineWidth, prevPoint[1] + 0.5 - lineWidth, 
365                               "L", prevPoint[0] - width + lineWidth, prevPoint[1] + 0.5 - lineWidth,
366                               "L", point[0] - width + lineWidth, point[1] + 0.5 + lineWidth,
367                               "L", point[0] - lineWidth, point[1] + 0.5 + lineWidth, "Z");
368                 }
369                 else if (position == 'top') {
370                     path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + lineWidth, 
371                               "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + width - lineWidth,
372                               "L", point[0] + 0.5 - lineWidth, point[1] + 1 + width - lineWidth,
373                               "L", point[0] + 0.5 - lineWidth, point[1] + 1 + lineWidth, "Z");
374                 }
375                 else {
376                     path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - lineWidth, 
377                             "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - width + lineWidth,
378                             "L", point[0] + 0.5 - lineWidth, point[1] - width + lineWidth,
379                             "L", point[0] + 0.5 - lineWidth, point[1] - lineWidth, "Z");
380                 }
381             } else {
382                 if (position == 'left') {
383                     path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", width, 0]);
384                 }
385                 else if (position == 'right') {
386                     path = path.concat(["M", point[0] - 0.5, point[1] + 0.5, "l", -width, 0]);
387                 }
388                 else if (position == 'top') {
389                     path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", 0, width]);
390                 }
391                 else {
392                     path = path.concat(["M", point[0] + 0.5, point[1] - 0.5, "l", 0, -width]);
393                 }
394             }
395         }
396         if (odd || even) {
397             if (oddPath.length) {
398                 if (!me.gridOdd && oddPath.length) {
399                     me.gridOdd = surface.add({
400                         type: 'path',
401                         path: oddPath
402                     });
403                 }
404                 me.gridOdd.setAttributes(Ext.apply({
405                     path: oddPath,
406                     hidden: false
407                 }, odd || {}), true);
408             }
409             if (evenPath.length) {
410                 if (!me.gridEven) {
411                     me.gridEven = surface.add({
412                         type: 'path',
413                         path: evenPath
414                     });
415                 } 
416                 me.gridEven.setAttributes(Ext.apply({
417                     path: evenPath,
418                     hidden: false
419                 }, even || {}), true);
420             }
421         }
422         else {
423             if (path.length) {
424                 if (!me.gridLines) {
425                     me.gridLines = me.chart.surface.add({
426                         type: 'path',
427                         path: path,
428                         "stroke-width": me.lineWidth || 1,
429                         stroke: me.gridColor || '#ccc'
430                     });
431                 }
432                 me.gridLines.setAttributes({
433                     hidden: false,
434                     path: path
435                 }, true);
436             }
437             else if (me.gridLines) {
438                 me.gridLines.hide(true);
439             }
440         }
441     },
442
443     //@private
444     getOrCreateLabel: function(i, text) {
445         var me = this,
446             labelGroup = me.labelGroup,
447             textLabel = labelGroup.getAt(i),
448             surface = me.chart.surface;
449         if (textLabel) {
450             if (text != textLabel.attr.text) {
451                 textLabel.setAttributes(Ext.apply({
452                     text: text
453                 }, me.label), true);
454                 textLabel._bbox = textLabel.getBBox();
455             }
456         }
457         else {
458             textLabel = surface.add(Ext.apply({
459                 group: labelGroup,
460                 type: 'text',
461                 x: 0,
462                 y: 0,
463                 text: text
464             }, me.label));
465             surface.renderItem(textLabel);
466             textLabel._bbox = textLabel.getBBox();
467         }
468         //get untransformed bounding box
469         if (me.label.rotation) {
470             textLabel.setAttributes({
471                 rotation: {
472                     degrees: 0    
473                 }    
474             }, true);
475             textLabel._ubbox = textLabel.getBBox();
476             textLabel.setAttributes(me.label, true);
477         } else {
478             textLabel._ubbox = textLabel._bbox;
479         }
480         return textLabel;
481     },
482     
483     rect2pointArray: function(sprite) {
484         var surface = this.chart.surface,
485             rect = surface.getBBox(sprite, true),
486             p1 = [rect.x, rect.y],
487             p1p = p1.slice(),
488             p2 = [rect.x + rect.width, rect.y],
489             p2p = p2.slice(),
490             p3 = [rect.x + rect.width, rect.y + rect.height],
491             p3p = p3.slice(),
492             p4 = [rect.x, rect.y + rect.height],
493             p4p = p4.slice(),
494             matrix = sprite.matrix;
495         //transform the points
496         p1[0] = matrix.x.apply(matrix, p1p);
497         p1[1] = matrix.y.apply(matrix, p1p);
498         
499         p2[0] = matrix.x.apply(matrix, p2p);
500         p2[1] = matrix.y.apply(matrix, p2p);
501         
502         p3[0] = matrix.x.apply(matrix, p3p);
503         p3[1] = matrix.y.apply(matrix, p3p);
504         
505         p4[0] = matrix.x.apply(matrix, p4p);
506         p4[1] = matrix.y.apply(matrix, p4p);
507         return [p1, p2, p3, p4];
508     },
509     
510     intersect: function(l1, l2) {
511         var r1 = this.rect2pointArray(l1),
512             r2 = this.rect2pointArray(l2);
513         return !!Ext.draw.Draw.intersect(r1, r2).length;
514     },
515     
516     drawHorizontalLabels: function() {
517        var  me = this,
518             labelConf = me.label,
519             floor = Math.floor,
520             max = Math.max,
521             axes = me.chart.axes,
522             position = me.position,
523             inflections = me.inflections,
524             ln = inflections.length,
525             labels = me.labels,
526             labelGroup = me.labelGroup,
527             maxHeight = 0,
528             ratio,
529             gutterY = me.chart.maxGutter[1],
530             ubbox, bbox, point, prevX, prevLabel,
531             projectedWidth = 0,
532             textLabel, attr, textRight, text,
533             label, last, x, y, i, firstLabel;
534
535         last = ln - 1;
536         //get a reference to the first text label dimensions
537         point = inflections[0];
538         firstLabel = me.getOrCreateLabel(0, me.label.renderer(labels[0]));
539         ratio = Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)) >> 0;
540         
541         for (i = 0; i < ln; i++) {
542             point = inflections[i];
543             text = me.label.renderer(labels[i]);
544             textLabel = me.getOrCreateLabel(i, text);
545             bbox = textLabel._bbox;
546             maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding);
547             x = floor(point[0] - (ratio? bbox.height : bbox.width) / 2);
548             if (me.chart.maxGutter[0] == 0) {
549                 if (i == 0 && axes.findIndex('position', 'left') == -1) {
550                     x = point[0];
551                 }
552                 else if (i == last && axes.findIndex('position', 'right') == -1) {
553                     x = point[0] - bbox.width;
554                 }
555             }
556             if (position == 'top') {
557                 y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2);
558             }
559             else {
560                 y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2);
561             }
562             
563             textLabel.setAttributes({
564                 hidden: false,
565                 x: x,
566                 y: y
567             }, true);
568
569             // Skip label if there isn't available minimum space
570             if (i != 0 && (me.intersect(textLabel, prevLabel)
571                 || me.intersect(textLabel, firstLabel))) {
572                 textLabel.hide(true);
573                 continue;
574             }
575             
576             prevLabel = textLabel;
577         }
578
579         return maxHeight;
580     },
581     
582     drawVerticalLabels: function() {
583         var me = this,
584             inflections = me.inflections,
585             position = me.position,
586             ln = inflections.length,
587             labels = me.labels,
588             maxWidth = 0,
589             max = Math.max,
590             floor = Math.floor,
591             ceil = Math.ceil,
592             axes = me.chart.axes,
593             gutterY = me.chart.maxGutter[1],
594             ubbox, bbox, point, prevLabel,
595             projectedWidth = 0,
596             textLabel, attr, textRight, text,
597             label, last, x, y, i;
598
599         last = ln;
600         for (i = 0; i < last; i++) {
601             point = inflections[i];
602             text = me.label.renderer(labels[i]);
603             textLabel = me.getOrCreateLabel(i, text);
604             bbox = textLabel._bbox;
605             
606             maxWidth = max(maxWidth, bbox.width + me.dashSize + me.label.padding);
607             y = point[1];
608             if (gutterY < bbox.height / 2) {
609                 if (i == last - 1 && axes.findIndex('position', 'top') == -1) {
610                     y = me.y - me.length + ceil(bbox.height / 2);
611                 }
612                 else if (i == 0 && axes.findIndex('position', 'bottom') == -1) {
613                     y = me.y - floor(bbox.height / 2);
614                 }
615             }
616             if (position == 'left') {
617                 x = point[0] - bbox.width - me.dashSize - me.label.padding - 2;
618             }
619             else {
620                 x = point[0] + me.dashSize + me.label.padding + 2;
621             }    
622             textLabel.setAttributes(Ext.apply({
623                 hidden: false,
624                 x: x,
625                 y: y
626             }, me.label), true);
627             // Skip label if there isn't available minimum space
628             if (i != 0 && me.intersect(textLabel, prevLabel)) {
629                 textLabel.hide(true);
630                 continue;
631             }
632             prevLabel = textLabel;
633         }
634         
635         return maxWidth;
636     },
637
638     /**
639      * Renders the labels in the axes.
640      */
641     drawLabel: function() {
642         var me = this,
643             position = me.position,
644             labelGroup = me.labelGroup,
645             inflections = me.inflections,
646             maxWidth = 0,
647             maxHeight = 0,
648             ln, i;
649
650         if (position == 'left' || position == 'right') {
651             maxWidth = me.drawVerticalLabels();    
652         } else {
653             maxHeight = me.drawHorizontalLabels();
654         }
655
656         // Hide unused bars
657         ln = labelGroup.getCount();
658         i = inflections.length;
659         for (; i < ln; i++) {
660             labelGroup.getAt(i).hide(true);
661         }
662
663         me.bbox = {};
664         Ext.apply(me.bbox, me.axisBBox);
665         me.bbox.height = maxHeight;
666         me.bbox.width = maxWidth;
667         if (Ext.isString(me.title)) {
668             me.drawTitle(maxWidth, maxHeight);
669         }
670     },
671
672     // @private creates the elipsis for the text.
673     elipsis: function(sprite, text, desiredWidth, minWidth, center) {
674         var bbox,
675             x;
676
677         if (desiredWidth < minWidth) {
678             sprite.hide(true);
679             return false;
680         }
681         while (text.length > 4) {
682             text = text.substr(0, text.length - 4) + "...";
683             sprite.setAttributes({
684                 text: text
685             }, true);
686             bbox = sprite.getBBox();
687             if (bbox.width < desiredWidth) {
688                 if (typeof center == 'number') {
689                     sprite.setAttributes({
690                         x: Math.floor(center - (bbox.width / 2))
691                     }, true);
692                 }
693                 break;
694             }
695         }
696         return true;
697     },
698
699     /**
700      * Updates the {@link #title} of this axis.
701      * @param {String} title
702      */
703     setTitle: function(title) {
704         this.title = title;
705         this.drawLabel();
706     },
707
708     // @private draws the title for the axis.
709     drawTitle: function(maxWidth, maxHeight) {
710         var me = this,
711             position = me.position,
712             surface = me.chart.surface,
713             displaySprite = me.displaySprite,
714             title = me.title,
715             rotate = (position == 'left' || position == 'right'),
716             x = me.x,
717             y = me.y,
718             base, bbox, pad;
719
720         if (displaySprite) {
721             displaySprite.setAttributes({text: title}, true);
722         } else {
723             base = {
724                 type: 'text',
725                 x: 0,
726                 y: 0,
727                 text: title
728             };
729             displaySprite = me.displaySprite = surface.add(Ext.apply(base, me.axisTitleStyle, me.labelTitle));
730             surface.renderItem(displaySprite);
731         }
732         bbox = displaySprite.getBBox();
733         pad = me.dashSize + me.label.padding;
734
735         if (rotate) {
736             y -= ((me.length / 2) - (bbox.height / 2));
737             if (position == 'left') {
738                 x -= (maxWidth + pad + (bbox.width / 2));
739             }
740             else {
741                 x += (maxWidth + pad + bbox.width - (bbox.width / 2));
742             }
743             me.bbox.width += bbox.width + 10;
744         }
745         else {
746             x += (me.length / 2) - (bbox.width * 0.5);
747             if (position == 'top') {
748                 y -= (maxHeight + pad + (bbox.height * 0.3));
749             }
750             else {
751                 y += (maxHeight + pad + (bbox.height * 0.8));
752             }
753             me.bbox.height += bbox.height + 10;
754         }
755         displaySprite.setAttributes({
756             translate: {
757                 x: x,
758                 y: y
759             }
760         }, true);
761     }
762 });