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