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.
122 * @cfg {String} title
123 * The title for the Axis
126 //@private force min/max values from store
130 * @cfg {Number} dashSize
131 * The size of the dash marker. Default's 3.
136 * @cfg {String} position
137 * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`.
145 * @cfg {Number} length
146 * Offset axis position. Default's 0.
151 * @cfg {Number} width
152 * Offset axis width. Default's 0.
156 majorTickSteps: false,
159 applyData: Ext.emptyFn,
161 getRange: function () {
163 store = me.chart.getChartStore(),
170 min = isNaN(me.minimum) ? Infinity : me.minimum,
171 max = isNaN(me.maximum) ? -Infinity : me.maximum,
172 total = 0, i, l, value, values, rec,
174 series = me.chart.series.items;
176 //if one series is stacked I have to aggregate the values
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
184 for (i = 0, l = series.length; !aggregate && i < l; i++) {
185 aggregate = aggregate || series[i].stacked;
186 excludes = series[i].__excludes || excludes;
188 store.each(function(record) {
190 if (!isFinite(min)) {
193 for (values = [0, 0], i = 0; i < ln; i++) {
197 rec = record.get(fields[i]);
198 values[+(rec > 0)] += math.abs(rec);
200 max = mmax(max, -values[0], +values[1]);
201 min = mmin(min, -values[0], +values[1]);
204 for (i = 0; i < ln; i++) {
208 value = record.get(fields[i]);
209 max = mmax(max, +value);
210 min = mmin(min, +value);
214 if (!isFinite(max)) {
215 max = me.prevMax || 0;
217 if (!isFinite(min)) {
218 min = me.prevMin || 0;
220 //normalize min max for snapEnds.
221 if (min != max && (max != Math.floor(max))) {
222 max = Math.floor(max) + 1;
225 if (!isNaN(me.minimum)) {
229 if (!isNaN(me.maximum)) {
233 return {min: min, max: max};
236 // @private creates a structure with start, end and step points.
237 calcEnds: function() {
240 range = me.getRange(),
245 out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ? (me.majorTickSteps +1) : me.steps);
248 if (me.forceMinMax) {
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.
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;
267 //Adjust after adjusting minimum and maximum
268 out.step = (out.to - out.from) / (outto - outfrom) * out.step;
270 if (me.adjustMaximumByMajorUnit) {
273 if (me.adjustMinimumByMajorUnit) {
274 out.from -= out.step;
276 me.prevMin = min == max? 0 : min;
282 * Renders the axis into the screen and updates its position.
284 drawAxis: function (init) {
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,
295 position = me.position,
298 stepCalcs = me.applyData(),
299 step = stepCalcs.step,
300 steps = stepCalcs.steps,
301 from = stepCalcs.from,
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)) {
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);
327 currentY = Math.floor(y) + 0.5;
328 path = ["M", x, currentY, "l", length, 0];
329 trueLength = length - (gutterX * 2);
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') {
337 me.labels = [stepCalcs.from];
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);
349 inflections.push([ Math.floor(x), Math.floor(currentY) ]);
352 me.labels.push(me.labels[me.labels.length -1] + step);
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);
363 inflections.push([ Math.floor(x), Math.floor(currentY) ]);
365 me.labels.push(me.labels[me.labels.length -1] + step);
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);
378 inflections.push([ Math.floor(currentX), Math.floor(y) ]);
381 me.labels.push(me.labels[me.labels.length -1] + step);
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);
392 inflections.push([ Math.floor(currentX), Math.floor(y) ]);
394 me.labels.push(me.labels[me.labels.length -1] + step);
399 me.axis = me.chart.surface.add(Ext.apply({
404 me.axis.setAttributes({
407 me.inflections = inflections;
408 if (!init && me.grid) {
411 me.axisBBox = me.axis.getBBox();
416 * Renders an horizontal and/or vertical grid into the Surface.
418 drawGrid: function() {
420 surface = me.chart.surface,
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,
432 path = [], styles, lineWidth, dlineWidth,
433 oddPath = [], evenPath = [];
435 if ((gutter[1] !== 0 && (position == 'left' || position == 'right')) ||
436 (gutter[0] !== 0 && (position == 'top' || position == 'bottom'))) {
440 for (; i < ln; i++) {
441 point = inflections[i];
442 prevPoint = inflections[i - 1];
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");
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");
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");
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");
473 if (position == 'left') {
474 path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", width, 0]);
476 else if (position == 'right') {
477 path = path.concat(["M", point[0] - 0.5, point[1] + 0.5, "l", -width, 0]);
479 else if (position == 'top') {
480 path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", 0, width]);
483 path = path.concat(["M", point[0] + 0.5, point[1] - 0.5, "l", 0, -width]);
488 if (oddPath.length) {
489 if (!me.gridOdd && oddPath.length) {
490 me.gridOdd = surface.add({
495 me.gridOdd.setAttributes(Ext.apply({
498 }, odd || {}), true);
500 if (evenPath.length) {
502 me.gridEven = surface.add({
507 me.gridEven.setAttributes(Ext.apply({
510 }, even || {}), true);
516 me.gridLines = me.chart.surface.add({
519 "stroke-width": me.lineWidth || 1,
520 stroke: me.gridColor || '#ccc'
523 me.gridLines.setAttributes({
528 else if (me.gridLines) {
529 me.gridLines.hide(true);
535 getOrCreateLabel: function(i, text) {
537 labelGroup = me.labelGroup,
538 textLabel = labelGroup.getAt(i),
539 surface = me.chart.surface;
541 if (text != textLabel.attr.text) {
542 textLabel.setAttributes(Ext.apply({
545 textLabel._bbox = textLabel.getBBox();
549 textLabel = surface.add(Ext.apply({
556 surface.renderItem(textLabel);
557 textLabel._bbox = textLabel.getBBox();
559 //get untransformed bounding box
560 if (me.label.rotation) {
561 textLabel.setAttributes({
566 textLabel._ubbox = textLabel.getBBox();
567 textLabel.setAttributes(me.label, true);
569 textLabel._ubbox = textLabel._bbox;
574 rect2pointArray: function(sprite) {
575 var surface = this.chart.surface,
576 rect = surface.getBBox(sprite, true),
577 p1 = [rect.x, rect.y],
579 p2 = [rect.x + rect.width, rect.y],
581 p3 = [rect.x + rect.width, rect.y + rect.height],
583 p4 = [rect.x, rect.y + rect.height],
585 matrix = sprite.matrix;
586 //transform the points
587 p1[0] = matrix.x.apply(matrix, p1p);
588 p1[1] = matrix.y.apply(matrix, p1p);
590 p2[0] = matrix.x.apply(matrix, p2p);
591 p2[1] = matrix.y.apply(matrix, p2p);
593 p3[0] = matrix.x.apply(matrix, p3p);
594 p3[1] = matrix.y.apply(matrix, p3p);
596 p4[0] = matrix.x.apply(matrix, p4p);
597 p4[1] = matrix.y.apply(matrix, p4p);
598 return [p1, p2, p3, p4];
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;
607 drawHorizontalLabels: function() {
609 labelConf = me.label,
612 axes = me.chart.axes,
613 position = me.position,
614 inflections = me.inflections,
615 ln = inflections.length,
617 labelGroup = me.labelGroup,
620 gutterY = me.chart.maxGutter[1],
621 ubbox, bbox, point, prevX, prevLabel,
623 textLabel, attr, textRight, text,
624 label, last, x, y, i, firstLabel;
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)));
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) {
643 else if (i == last && axes.findIndex('position', 'right') == -1) {
644 x = point[0] - bbox.width;
647 if (position == 'top') {
648 y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2);
651 y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2);
654 textLabel.setAttributes({
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);
667 prevLabel = textLabel;
673 drawVerticalLabels: function() {
675 inflections = me.inflections,
676 position = me.position,
677 ln = inflections.length,
683 axes = me.chart.axes,
684 gutterY = me.chart.maxGutter[1],
685 ubbox, bbox, point, prevLabel,
687 textLabel, attr, textRight, text,
688 label, last, x, y, i;
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;
697 maxWidth = max(maxWidth, bbox.width + me.dashSize + me.label.padding);
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);
703 else if (i == 0 && axes.findIndex('position', 'bottom') == -1) {
704 y = me.y - floor(bbox.height / 2);
707 if (position == 'left') {
708 x = point[0] - bbox.width - me.dashSize - me.label.padding - 2;
711 x = point[0] + me.dashSize + me.label.padding + 2;
713 textLabel.setAttributes(Ext.apply({
718 // Skip label if there isn't available minimum space
719 if (i != 0 && me.intersect(textLabel, prevLabel)) {
720 textLabel.hide(true);
723 prevLabel = textLabel;
730 * Renders the labels in the axes.
732 drawLabel: function() {
734 position = me.position,
735 labelGroup = me.labelGroup,
736 inflections = me.inflections,
741 if (position == 'left' || position == 'right') {
742 maxWidth = me.drawVerticalLabels();
744 maxHeight = me.drawHorizontalLabels();
748 ln = labelGroup.getCount();
749 i = inflections.length;
750 for (; i < ln; i++) {
751 labelGroup.getAt(i).hide(true);
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);
763 // @private creates the elipsis for the text.
764 elipsis: function(sprite, text, desiredWidth, minWidth, center) {
768 if (desiredWidth < minWidth) {
772 while (text.length > 4) {
773 text = text.substr(0, text.length - 4) + "...";
774 sprite.setAttributes({
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))
791 * Updates the {@link #title} of this axis.
792 * @param {String} title
794 setTitle: function(title) {
799 // @private draws the title for the axis.
800 drawTitle: function(maxWidth, maxHeight) {
802 position = me.position,
803 surface = me.chart.surface,
804 displaySprite = me.displaySprite,
806 rotate = (position == 'left' || position == 'right'),
812 displaySprite.setAttributes({text: title}, true);
820 displaySprite = me.displaySprite = surface.add(Ext.apply(base, me.axisTitleStyle, me.labelTitle));
821 surface.renderItem(displaySprite);
823 bbox = displaySprite.getBBox();
824 pad = me.dashSize + me.label.padding;
827 y -= ((me.length / 2) - (bbox.height / 2));
828 if (position == 'left') {
829 x -= (maxWidth + pad + (bbox.width / 2));
832 x += (maxWidth + pad + bbox.width - (bbox.width / 2));
834 me.bbox.width += bbox.width + 10;
837 x += (me.length / 2) - (bbox.width * 0.5);
838 if (position == 'top') {
839 y -= (maxHeight + pad + (bbox.height * 0.3));
842 y += (maxHeight + pad + (bbox.height * 0.8));
844 me.bbox.height += bbox.height + 10;
846 displaySprite.setAttributes({