3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
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.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.chart.axis.Axis
17 * @extends Ext.chart.axis.Abstract
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:
29 * fields: ['data1', 'data2', 'data3'],
30 * title: 'Number of Hits',
44 * title: 'Month of the Year',
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.
58 Ext.define('Ext.chart.axis.Axis', {
60 /* Begin Definitions */
62 extend: 'Ext.chart.axis.Abstract',
64 alternateClassName: 'Ext.chart.Axis',
66 requires: ['Ext.draw.Draw'],
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.
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.
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}.
91 * fields: ['data1', 'data2', 'data3'],
92 * title: 'Number of Hits',
103 * position: 'bottom',
105 * title: 'Month of the Year',
112 * @cfg {Number} majorTickSteps
113 * If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value.
117 * @cfg {Number} minorTickSteps
118 * The number of small ticks between two major ticks. Default is zero.
121 //@private force min/max values from store
125 * @cfg {Number} dashSize
126 * The size of the dash marker. Default's 3.
131 * @cfg {String} position
132 * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`.
140 * @cfg {Number} length
141 * Offset axis position. Default's 0.
146 * @cfg {Number} width
147 * Offset axis width. Default's 0.
151 majorTickSteps: false,
154 applyData: Ext.emptyFn,
156 // @private creates a structure with start, end and step points.
157 calcEnds: function() {
162 store = me.chart.substore || me.chart.store,
163 series = me.chart.series.items,
166 min = isNaN(me.minimum) ? Infinity : me.minimum,
167 max = isNaN(me.maximum) ? -Infinity : me.maximum,
168 prevMin = me.prevMin,
169 prevMax = me.prevMax,
174 i, l, values, rec, out;
176 //if one series is stacked I have to aggregate the values
178 for (i = 0, l = series.length; !aggregate && i < l; i++) {
179 aggregate = aggregate || series[i].stacked;
180 excludes = series[i].__excludes || excludes;
182 store.each(function(record) {
184 if (!isFinite(min)) {
187 for (values = [0, 0], i = 0; i < ln; i++) {
191 rec = record.get(fields[i]);
192 values[+(rec > 0)] += math.abs(rec);
194 max = mmax(max, -values[0], values[1]);
195 min = mmin(min, -values[0], values[1]);
198 for (i = 0; i < ln; i++) {
202 value = record.get(fields[i]);
203 max = mmax(max, value);
204 min = mmin(min, value);
208 if (!isFinite(max)) {
209 max = me.prevMax || 0;
211 if (!isFinite(min)) {
212 min = me.prevMin || 0;
214 //normalize min max for snapEnds.
215 if (min != max && (max != (max >> 0))) {
216 max = (max >> 0) + 1;
218 out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ? (me.majorTickSteps +1) : me.steps);
221 if (me.forceMinMax) {
229 if (!isNaN(me.maximum)) {
230 //TODO(nico) users are responsible for their own minimum/maximum values set.
231 //Clipping should be added to remove lines in the chart which are below the axis.
234 if (!isNaN(me.minimum)) {
235 //TODO(nico) users are responsible for their own minimum/maximum values set.
236 //Clipping should be added to remove lines in the chart which are below the axis.
237 out.from = me.minimum;
240 //Adjust after adjusting minimum and maximum
241 out.step = (out.to - out.from) / (outto - outfrom) * out.step;
243 if (me.adjustMaximumByMajorUnit) {
246 if (me.adjustMinimumByMajorUnit) {
247 out.from -= out.step;
249 me.prevMin = min == max? 0 : min;
255 * Renders the axis into the screen and updates it's position.
257 drawAxis: function (init) {
262 gutterX = me.chart.maxGutter[0],
263 gutterY = me.chart.maxGutter[1],
264 dashSize = me.dashSize,
265 subDashesX = me.minorTickSteps || 0,
266 subDashesY = me.minorTickSteps || 0,
268 position = me.position,
271 stepCalcs = me.applyData(),
272 step = stepCalcs.step,
273 steps = stepCalcs.steps,
274 from = stepCalcs.from,
285 //If no steps are specified
286 //then don't draw the axis. This generally happens
287 //when an empty store.
288 if (me.hidden || isNaN(step) || (from == to)) {
292 me.from = stepCalcs.from;
293 me.to = stepCalcs.to;
294 if (position == 'left' || position == 'right') {
295 currentX = Math.floor(x) + 0.5;
296 path = ["M", currentX, y, "l", 0, -length];
297 trueLength = length - (gutterY * 2);
300 currentY = Math.floor(y) + 0.5;
301 path = ["M", x, currentY, "l", length, 0];
302 trueLength = length - (gutterX * 2);
305 delta = trueLength / (steps || 1);
306 dashesX = Math.max(subDashesX +1, 0);
307 dashesY = Math.max(subDashesY +1, 0);
308 if (me.type == 'Numeric') {
310 me.labels = [stepCalcs.from];
312 if (position == 'right' || position == 'left') {
313 currentY = y - gutterY;
314 currentX = x - ((position == 'left') * dashSize * 2);
315 while (currentY >= y - gutterY - trueLength) {
316 path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashSize * 2 + 1, 0);
317 if (currentY != y - gutterY) {
318 for (i = 1; i < dashesY; i++) {
319 path.push("M", currentX + dashSize, Math.floor(currentY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
322 inflections.push([ Math.floor(x), Math.floor(currentY) ]);
325 me.labels.push(me.labels[me.labels.length -1] + step);
331 if (Math.round(currentY + delta - (y - gutterY - trueLength))) {
332 path.push("M", currentX, Math.floor(y - length + gutterY) + 0.5, "l", dashSize * 2 + 1, 0);
333 for (i = 1; i < dashesY; i++) {
334 path.push("M", currentX + dashSize, Math.floor(y - length + gutterY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
336 inflections.push([ Math.floor(x), Math.floor(currentY) ]);
338 me.labels.push(me.labels[me.labels.length -1] + step);
342 currentX = x + gutterX;
343 currentY = y - ((position == 'top') * dashSize * 2);
344 while (currentX <= x + gutterX + trueLength) {
345 path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
346 if (currentX != x + gutterX) {
347 for (i = 1; i < dashesX; i++) {
348 path.push("M", Math.floor(currentX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
351 inflections.push([ Math.floor(currentX), Math.floor(y) ]);
354 me.labels.push(me.labels[me.labels.length -1] + step);
360 if (Math.round(currentX - delta - (x + gutterX + trueLength))) {
361 path.push("M", Math.floor(x + length - gutterX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
362 for (i = 1; i < dashesX; i++) {
363 path.push("M", Math.floor(x + length - gutterX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
365 inflections.push([ Math.floor(currentX), Math.floor(y) ]);
367 me.labels.push(me.labels[me.labels.length -1] + step);
372 me.axis = me.chart.surface.add(Ext.apply({
377 me.axis.setAttributes({
380 me.inflections = inflections;
381 if (!init && me.grid) {
384 me.axisBBox = me.axis.getBBox();
389 * Renders an horizontal and/or vertical grid into the Surface.
391 drawGrid: function() {
393 surface = me.chart.surface,
397 inflections = me.inflections,
398 ln = inflections.length - ((odd || even)? 0 : 1),
399 position = me.position,
400 gutter = me.chart.maxGutter,
401 width = me.width - 2,
405 path = [], styles, lineWidth, dlineWidth,
406 oddPath = [], evenPath = [];
408 if ((gutter[1] !== 0 && (position == 'left' || position == 'right')) ||
409 (gutter[0] !== 0 && (position == 'top' || position == 'bottom'))) {
413 for (; i < ln; i++) {
414 point = inflections[i];
415 prevPoint = inflections[i - 1];
417 path = (i % 2)? oddPath : evenPath;
418 styles = ((i % 2)? odd : even) || {};
419 lineWidth = (styles.lineWidth || styles['stroke-width'] || 0) / 2;
420 dlineWidth = 2 * lineWidth;
421 if (position == 'left') {
422 path.push("M", prevPoint[0] + 1 + lineWidth, prevPoint[1] + 0.5 - lineWidth,
423 "L", prevPoint[0] + 1 + width - lineWidth, prevPoint[1] + 0.5 - lineWidth,
424 "L", point[0] + 1 + width - lineWidth, point[1] + 0.5 + lineWidth,
425 "L", point[0] + 1 + lineWidth, point[1] + 0.5 + lineWidth, "Z");
427 else if (position == 'right') {
428 path.push("M", prevPoint[0] - lineWidth, prevPoint[1] + 0.5 - lineWidth,
429 "L", prevPoint[0] - width + lineWidth, prevPoint[1] + 0.5 - lineWidth,
430 "L", point[0] - width + lineWidth, point[1] + 0.5 + lineWidth,
431 "L", point[0] - lineWidth, point[1] + 0.5 + lineWidth, "Z");
433 else if (position == 'top') {
434 path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + lineWidth,
435 "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + width - lineWidth,
436 "L", point[0] + 0.5 - lineWidth, point[1] + 1 + width - lineWidth,
437 "L", point[0] + 0.5 - lineWidth, point[1] + 1 + lineWidth, "Z");
440 path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - lineWidth,
441 "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - width + lineWidth,
442 "L", point[0] + 0.5 - lineWidth, point[1] - width + lineWidth,
443 "L", point[0] + 0.5 - lineWidth, point[1] - lineWidth, "Z");
446 if (position == 'left') {
447 path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", width, 0]);
449 else if (position == 'right') {
450 path = path.concat(["M", point[0] - 0.5, point[1] + 0.5, "l", -width, 0]);
452 else if (position == 'top') {
453 path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", 0, width]);
456 path = path.concat(["M", point[0] + 0.5, point[1] - 0.5, "l", 0, -width]);
461 if (oddPath.length) {
462 if (!me.gridOdd && oddPath.length) {
463 me.gridOdd = surface.add({
468 me.gridOdd.setAttributes(Ext.apply({
471 }, odd || {}), true);
473 if (evenPath.length) {
475 me.gridEven = surface.add({
480 me.gridEven.setAttributes(Ext.apply({
483 }, even || {}), true);
489 me.gridLines = me.chart.surface.add({
492 "stroke-width": me.lineWidth || 1,
493 stroke: me.gridColor || '#ccc'
496 me.gridLines.setAttributes({
501 else if (me.gridLines) {
502 me.gridLines.hide(true);
508 getOrCreateLabel: function(i, text) {
510 labelGroup = me.labelGroup,
511 textLabel = labelGroup.getAt(i),
512 surface = me.chart.surface;
514 if (text != textLabel.attr.text) {
515 textLabel.setAttributes(Ext.apply({
518 textLabel._bbox = textLabel.getBBox();
522 textLabel = surface.add(Ext.apply({
529 surface.renderItem(textLabel);
530 textLabel._bbox = textLabel.getBBox();
532 //get untransformed bounding box
533 if (me.label.rotation) {
534 textLabel.setAttributes({
539 textLabel._ubbox = textLabel.getBBox();
540 textLabel.setAttributes(me.label, true);
542 textLabel._ubbox = textLabel._bbox;
547 rect2pointArray: function(sprite) {
548 var surface = this.chart.surface,
549 rect = surface.getBBox(sprite, true),
550 p1 = [rect.x, rect.y],
552 p2 = [rect.x + rect.width, rect.y],
554 p3 = [rect.x + rect.width, rect.y + rect.height],
556 p4 = [rect.x, rect.y + rect.height],
558 matrix = sprite.matrix;
559 //transform the points
560 p1[0] = matrix.x.apply(matrix, p1p);
561 p1[1] = matrix.y.apply(matrix, p1p);
563 p2[0] = matrix.x.apply(matrix, p2p);
564 p2[1] = matrix.y.apply(matrix, p2p);
566 p3[0] = matrix.x.apply(matrix, p3p);
567 p3[1] = matrix.y.apply(matrix, p3p);
569 p4[0] = matrix.x.apply(matrix, p4p);
570 p4[1] = matrix.y.apply(matrix, p4p);
571 return [p1, p2, p3, p4];
574 intersect: function(l1, l2) {
575 var r1 = this.rect2pointArray(l1),
576 r2 = this.rect2pointArray(l2);
577 return !!Ext.draw.Draw.intersect(r1, r2).length;
580 drawHorizontalLabels: function() {
582 labelConf = me.label,
585 axes = me.chart.axes,
586 position = me.position,
587 inflections = me.inflections,
588 ln = inflections.length,
590 labelGroup = me.labelGroup,
593 gutterY = me.chart.maxGutter[1],
594 ubbox, bbox, point, prevX, prevLabel,
596 textLabel, attr, textRight, text,
597 label, last, x, y, i, firstLabel;
600 //get a reference to the first text label dimensions
601 point = inflections[0];
602 firstLabel = me.getOrCreateLabel(0, me.label.renderer(labels[0]));
603 ratio = Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)) >> 0;
605 for (i = 0; i < ln; i++) {
606 point = inflections[i];
607 text = me.label.renderer(labels[i]);
608 textLabel = me.getOrCreateLabel(i, text);
609 bbox = textLabel._bbox;
610 maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding);
611 x = floor(point[0] - (ratio? bbox.height : bbox.width) / 2);
612 if (me.chart.maxGutter[0] == 0) {
613 if (i == 0 && axes.findIndex('position', 'left') == -1) {
616 else if (i == last && axes.findIndex('position', 'right') == -1) {
617 x = point[0] - bbox.width;
620 if (position == 'top') {
621 y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2);
624 y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2);
627 textLabel.setAttributes({
633 // Skip label if there isn't available minimum space
634 if (i != 0 && (me.intersect(textLabel, prevLabel)
635 || me.intersect(textLabel, firstLabel))) {
636 textLabel.hide(true);
640 prevLabel = textLabel;
646 drawVerticalLabels: function() {
648 inflections = me.inflections,
649 position = me.position,
650 ln = inflections.length,
656 axes = me.chart.axes,
657 gutterY = me.chart.maxGutter[1],
658 ubbox, bbox, point, prevLabel,
660 textLabel, attr, textRight, text,
661 label, last, x, y, i;
664 for (i = 0; i < last; i++) {
665 point = inflections[i];
666 text = me.label.renderer(labels[i]);
667 textLabel = me.getOrCreateLabel(i, text);
668 bbox = textLabel._bbox;
670 maxWidth = max(maxWidth, bbox.width + me.dashSize + me.label.padding);
672 if (gutterY < bbox.height / 2) {
673 if (i == last - 1 && axes.findIndex('position', 'top') == -1) {
674 y = me.y - me.length + ceil(bbox.height / 2);
676 else if (i == 0 && axes.findIndex('position', 'bottom') == -1) {
677 y = me.y - floor(bbox.height / 2);
680 if (position == 'left') {
681 x = point[0] - bbox.width - me.dashSize - me.label.padding - 2;
684 x = point[0] + me.dashSize + me.label.padding + 2;
686 textLabel.setAttributes(Ext.apply({
691 // Skip label if there isn't available minimum space
692 if (i != 0 && me.intersect(textLabel, prevLabel)) {
693 textLabel.hide(true);
696 prevLabel = textLabel;
703 * Renders the labels in the axes.
705 drawLabel: function() {
707 position = me.position,
708 labelGroup = me.labelGroup,
709 inflections = me.inflections,
714 if (position == 'left' || position == 'right') {
715 maxWidth = me.drawVerticalLabels();
717 maxHeight = me.drawHorizontalLabels();
721 ln = labelGroup.getCount();
722 i = inflections.length;
723 for (; i < ln; i++) {
724 labelGroup.getAt(i).hide(true);
728 Ext.apply(me.bbox, me.axisBBox);
729 me.bbox.height = maxHeight;
730 me.bbox.width = maxWidth;
731 if (Ext.isString(me.title)) {
732 me.drawTitle(maxWidth, maxHeight);
736 // @private creates the elipsis for the text.
737 elipsis: function(sprite, text, desiredWidth, minWidth, center) {
741 if (desiredWidth < minWidth) {
745 while (text.length > 4) {
746 text = text.substr(0, text.length - 4) + "...";
747 sprite.setAttributes({
750 bbox = sprite.getBBox();
751 if (bbox.width < desiredWidth) {
752 if (typeof center == 'number') {
753 sprite.setAttributes({
754 x: Math.floor(center - (bbox.width / 2))
764 * Updates the {@link #title} of this axis.
765 * @param {String} title
767 setTitle: function(title) {
772 // @private draws the title for the axis.
773 drawTitle: function(maxWidth, maxHeight) {
775 position = me.position,
776 surface = me.chart.surface,
777 displaySprite = me.displaySprite,
779 rotate = (position == 'left' || position == 'right'),
785 displaySprite.setAttributes({text: title}, true);
793 displaySprite = me.displaySprite = surface.add(Ext.apply(base, me.axisTitleStyle, me.labelTitle));
794 surface.renderItem(displaySprite);
796 bbox = displaySprite.getBBox();
797 pad = me.dashSize + me.label.padding;
800 y -= ((me.length / 2) - (bbox.height / 2));
801 if (position == 'left') {
802 x -= (maxWidth + pad + (bbox.width / 2));
805 x += (maxWidth + pad + bbox.width - (bbox.width / 2));
807 me.bbox.width += bbox.width + 10;
810 x += (me.length / 2) - (bbox.width * 0.5);
811 if (position == 'top') {
812 y -= (maxHeight + pad + (bbox.height * 0.3));
815 y += (maxHeight + pad + (bbox.height * 0.8));
817 me.bbox.height += bbox.height + 10;
819 displaySprite.setAttributes({