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