/** * @class Ext.chart.axis.Axis * @extends Ext.chart.axis.Abstract * * Defines axis for charts. The axis position, type, style can be configured. * The axes are defined in an axes array of configuration objects where the type, * field, grid and other configuration options can be set. To know more about how * to create a Chart please check the Chart class documentation. Here's an example for the axes part: * An example of axis for a series (in this case for an area chart that has multiple layers of yFields) could be: * * axes: [{ * type: 'Numeric', * grid: true, * position: 'left', * fields: ['data1', 'data2', 'data3'], * title: 'Number of Hits', * grid: { * odd: { * opacity: 1, * fill: '#ddd', * stroke: '#bbb', * 'stroke-width': 1 * } * }, * minimum: 0 * }, { * type: 'Category', * position: 'bottom', * fields: ['name'], * title: 'Month of the Year', * grid: true, * label: { * rotate: { * degrees: 315 * } * } * }] * * 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 * 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. * Both the category and numeric axes have `grid` set, which means that horizontal and vertical lines will cover the chart background. In the * category axis the labels will be rotated so they can fit the space better. */ Ext.define('Ext.chart.axis.Axis', { /* Begin Definitions */ extend: 'Ext.chart.axis.Abstract', alternateClassName: 'Ext.chart.Axis', requires: ['Ext.draw.Draw'], /* End Definitions */ /** * @cfg {Number} majorTickSteps * If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value. */ /** * @cfg {Number} minorTickSteps * The number of small ticks between two major ticks. Default is zero. */ /** * @cfg {Number} dashSize * The size of the dash marker. Default's 3. */ dashSize: 3, /** * @cfg {String} position * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`. */ position: 'bottom', // @private skipFirst: false, /** * @cfg {Number} length * Offset axis position. Default's 0. */ length: 0, /** * @cfg {Number} width * Offset axis width. Default's 0. */ width: 0, majorTickSteps: false, // @private applyData: Ext.emptyFn, // @private creates a structure with start, end and step points. calcEnds: function() { var me = this, math = Math, mmax = math.max, mmin = math.min, store = me.chart.substore || me.chart.store, series = me.chart.series.items, fields = me.fields, ln = fields.length, min = isNaN(me.minimum) ? Infinity : me.minimum, max = isNaN(me.maximum) ? -Infinity : me.maximum, prevMin = me.prevMin, prevMax = me.prevMax, aggregate = false, total = 0, excludes = [], outfrom, outto, i, l, values, rec, out; //if one series is stacked I have to aggregate the values //for the scale. for (i = 0, l = series.length; !aggregate && i < l; i++) { aggregate = aggregate || series[i].stacked; excludes = series[i].__excludes || excludes; } store.each(function(record) { if (aggregate) { if (!isFinite(min)) { min = 0; } for (values = [0, 0], i = 0; i < ln; i++) { if (excludes[i]) { continue; } rec = record.get(fields[i]); values[+(rec > 0)] += math.abs(rec); } max = mmax(max, -values[0], values[1]); min = mmin(min, -values[0], values[1]); } else { for (i = 0; i < ln; i++) { if (excludes[i]) { continue; } value = record.get(fields[i]); max = mmax(max, value); min = mmin(min, value); } } }); if (!isFinite(max)) { max = me.prevMax || 0; } if (!isFinite(min)) { min = me.prevMin || 0; } //normalize min max for snapEnds. if (min != max && (max != (max >> 0))) { max = (max >> 0) + 1; } out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ? (me.majorTickSteps +1) : me.steps); outfrom = out.from; outto = out.to; if (!isNaN(me.maximum)) { //TODO(nico) users are responsible for their own minimum/maximum values set. //Clipping should be added to remove lines in the chart which are below the axis. out.to = me.maximum; } if (!isNaN(me.minimum)) { //TODO(nico) users are responsible for their own minimum/maximum values set. //Clipping should be added to remove lines in the chart which are below the axis. out.from = me.minimum; } //Adjust after adjusting minimum and maximum out.step = (out.to - out.from) / (outto - outfrom) * out.step; if (me.adjustMaximumByMajorUnit) { out.to += out.step; } if (me.adjustMinimumByMajorUnit) { out.from -= out.step; } me.prevMin = min == max? 0 : min; me.prevMax = max; return out; }, /** * Renders the axis into the screen and updates it's position. */ drawAxis: function (init) { var me = this, i, j, x = me.x, y = me.y, gutterX = me.chart.maxGutter[0], gutterY = me.chart.maxGutter[1], dashSize = me.dashSize, subDashesX = me.minorTickSteps || 0, subDashesY = me.minorTickSteps || 0, length = me.length, position = me.position, inflections = [], calcLabels = false, stepCalcs = me.applyData(), step = stepCalcs.step, steps = stepCalcs.steps, from = stepCalcs.from, to = stepCalcs.to, trueLength, currentX, currentY, path, prev, dashesX, dashesY, delta; //If no steps are specified //then don't draw the axis. This generally happens //when an empty store. if (me.hidden || isNaN(step) || (from == to)) { return; } me.from = stepCalcs.from; me.to = stepCalcs.to; if (position == 'left' || position == 'right') { currentX = Math.floor(x) + 0.5; path = ["M", currentX, y, "l", 0, -length]; trueLength = length - (gutterY * 2); } else { currentY = Math.floor(y) + 0.5; path = ["M", x, currentY, "l", length, 0]; trueLength = length - (gutterX * 2); } delta = trueLength / (steps || 1); dashesX = Math.max(subDashesX +1, 0); dashesY = Math.max(subDashesY +1, 0); if (me.type == 'Numeric') { calcLabels = true; me.labels = [stepCalcs.from]; } if (position == 'right' || position == 'left') { currentY = y - gutterY; currentX = x - ((position == 'left') * dashSize * 2); while (currentY >= y - gutterY - trueLength) { path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashSize * 2 + 1, 0); if (currentY != y - gutterY) { for (i = 1; i < dashesY; i++) { path.push("M", currentX + dashSize, Math.floor(currentY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0); } } inflections.push([ Math.floor(x), Math.floor(currentY) ]); currentY -= delta; if (calcLabels) { me.labels.push(me.labels[me.labels.length -1] + step); } if (delta === 0) { break; } } if (Math.round(currentY + delta - (y - gutterY - trueLength))) { path.push("M", currentX, Math.floor(y - length + gutterY) + 0.5, "l", dashSize * 2 + 1, 0); for (i = 1; i < dashesY; i++) { path.push("M", currentX + dashSize, Math.floor(y - length + gutterY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0); } inflections.push([ Math.floor(x), Math.floor(currentY) ]); if (calcLabels) { me.labels.push(me.labels[me.labels.length -1] + step); } } } else { currentX = x + gutterX; currentY = y - ((position == 'top') * dashSize * 2); while (currentX <= x + gutterX + trueLength) { path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashSize * 2 + 1); if (currentX != x + gutterX) { for (i = 1; i < dashesX; i++) { path.push("M", Math.floor(currentX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1); } } inflections.push([ Math.floor(currentX), Math.floor(y) ]); currentX += delta; if (calcLabels) { me.labels.push(me.labels[me.labels.length -1] + step); } if (delta === 0) { break; } } if (Math.round(currentX - delta - (x + gutterX + trueLength))) { path.push("M", Math.floor(x + length - gutterX) + 0.5, currentY, "l", 0, dashSize * 2 + 1); for (i = 1; i < dashesX; i++) { path.push("M", Math.floor(x + length - gutterX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1); } inflections.push([ Math.floor(currentX), Math.floor(y) ]); if (calcLabels) { me.labels.push(me.labels[me.labels.length -1] + step); } } } if (!me.axis) { me.axis = me.chart.surface.add(Ext.apply({ type: 'path', path: path }, me.axisStyle)); } me.axis.setAttributes({ path: path }, true); me.inflections = inflections; if (!init && me.grid) { me.drawGrid(); } me.axisBBox = me.axis.getBBox(); me.drawLabel(); }, /** * Renders an horizontal and/or vertical grid into the Surface. */ drawGrid: function() { var me = this, surface = me.chart.surface, grid = me.grid, odd = grid.odd, even = grid.even, inflections = me.inflections, ln = inflections.length - ((odd || even)? 0 : 1), position = me.position, gutter = me.chart.maxGutter, width = me.width - 2, vert = false, point, prevPoint, i = 1, path = [], styles, lineWidth, dlineWidth, oddPath = [], evenPath = []; if ((gutter[1] !== 0 && (position == 'left' || position == 'right')) || (gutter[0] !== 0 && (position == 'top' || position == 'bottom'))) { i = 0; ln++; } for (; i < ln; i++) { point = inflections[i]; prevPoint = inflections[i - 1]; if (odd || even) { path = (i % 2)? oddPath : evenPath; styles = ((i % 2)? odd : even) || {}; lineWidth = (styles.lineWidth || styles['stroke-width'] || 0) / 2; dlineWidth = 2 * lineWidth; if (position == 'left') { path.push("M", prevPoint[0] + 1 + lineWidth, prevPoint[1] + 0.5 - lineWidth, "L", prevPoint[0] + 1 + width - lineWidth, prevPoint[1] + 0.5 - lineWidth, "L", point[0] + 1 + width - lineWidth, point[1] + 0.5 + lineWidth, "L", point[0] + 1 + lineWidth, point[1] + 0.5 + lineWidth, "Z"); } else if (position == 'right') { path.push("M", prevPoint[0] - lineWidth, prevPoint[1] + 0.5 - lineWidth, "L", prevPoint[0] - width + lineWidth, prevPoint[1] + 0.5 - lineWidth, "L", point[0] - width + lineWidth, point[1] + 0.5 + lineWidth, "L", point[0] - lineWidth, point[1] + 0.5 + lineWidth, "Z"); } else if (position == 'top') { path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + lineWidth, "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + width - lineWidth, "L", point[0] + 0.5 - lineWidth, point[1] + 1 + width - lineWidth, "L", point[0] + 0.5 - lineWidth, point[1] + 1 + lineWidth, "Z"); } else { path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - lineWidth, "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - width + lineWidth, "L", point[0] + 0.5 - lineWidth, point[1] - width + lineWidth, "L", point[0] + 0.5 - lineWidth, point[1] - lineWidth, "Z"); } } else { if (position == 'left') { path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", width, 0]); } else if (position == 'right') { path = path.concat(["M", point[0] - 0.5, point[1] + 0.5, "l", -width, 0]); } else if (position == 'top') { path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", 0, width]); } else { path = path.concat(["M", point[0] + 0.5, point[1] - 0.5, "l", 0, -width]); } } } if (odd || even) { if (oddPath.length) { if (!me.gridOdd && oddPath.length) { me.gridOdd = surface.add({ type: 'path', path: oddPath }); } me.gridOdd.setAttributes(Ext.apply({ path: oddPath, hidden: false }, odd || {}), true); } if (evenPath.length) { if (!me.gridEven) { me.gridEven = surface.add({ type: 'path', path: evenPath }); } me.gridEven.setAttributes(Ext.apply({ path: evenPath, hidden: false }, even || {}), true); } } else { if (path.length) { if (!me.gridLines) { me.gridLines = me.chart.surface.add({ type: 'path', path: path, "stroke-width": me.lineWidth || 1, stroke: me.gridColor || '#ccc' }); } me.gridLines.setAttributes({ hidden: false, path: path }, true); } else if (me.gridLines) { me.gridLines.hide(true); } } }, //@private getOrCreateLabel: function(i, text) { var me = this, labelGroup = me.labelGroup, textLabel = labelGroup.getAt(i), surface = me.chart.surface; if (textLabel) { if (text != textLabel.attr.text) { textLabel.setAttributes(Ext.apply({ text: text }, me.label), true); textLabel._bbox = textLabel.getBBox(); } } else { textLabel = surface.add(Ext.apply({ group: labelGroup, type: 'text', x: 0, y: 0, text: text }, me.label)); surface.renderItem(textLabel); textLabel._bbox = textLabel.getBBox(); } //get untransformed bounding box if (me.label.rotation) { textLabel.setAttributes({ rotation: { degrees: 0 } }, true); textLabel._ubbox = textLabel.getBBox(); textLabel.setAttributes(me.label, true); } else { textLabel._ubbox = textLabel._bbox; } return textLabel; }, rect2pointArray: function(sprite) { var surface = this.chart.surface, rect = surface.getBBox(sprite, true), p1 = [rect.x, rect.y], p1p = p1.slice(), p2 = [rect.x + rect.width, rect.y], p2p = p2.slice(), p3 = [rect.x + rect.width, rect.y + rect.height], p3p = p3.slice(), p4 = [rect.x, rect.y + rect.height], p4p = p4.slice(), matrix = sprite.matrix; //transform the points p1[0] = matrix.x.apply(matrix, p1p); p1[1] = matrix.y.apply(matrix, p1p); p2[0] = matrix.x.apply(matrix, p2p); p2[1] = matrix.y.apply(matrix, p2p); p3[0] = matrix.x.apply(matrix, p3p); p3[1] = matrix.y.apply(matrix, p3p); p4[0] = matrix.x.apply(matrix, p4p); p4[1] = matrix.y.apply(matrix, p4p); return [p1, p2, p3, p4]; }, intersect: function(l1, l2) { var r1 = this.rect2pointArray(l1), r2 = this.rect2pointArray(l2); return !!Ext.draw.Draw.intersect(r1, r2).length; }, drawHorizontalLabels: function() { var me = this, labelConf = me.label, floor = Math.floor, max = Math.max, axes = me.chart.axes, position = me.position, inflections = me.inflections, ln = inflections.length, labels = me.labels, labelGroup = me.labelGroup, maxHeight = 0, ratio, gutterY = me.chart.maxGutter[1], ubbox, bbox, point, prevX, prevLabel, projectedWidth = 0, textLabel, attr, textRight, text, label, last, x, y, i, firstLabel; last = ln - 1; //get a reference to the first text label dimensions point = inflections[0]; firstLabel = me.getOrCreateLabel(0, me.label.renderer(labels[0])); ratio = Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)) >> 0; for (i = 0; i < ln; i++) { point = inflections[i]; text = me.label.renderer(labels[i]); textLabel = me.getOrCreateLabel(i, text); bbox = textLabel._bbox; maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding); x = floor(point[0] - (ratio? bbox.height : bbox.width) / 2); if (me.chart.maxGutter[0] == 0) { if (i == 0 && axes.findIndex('position', 'left') == -1) { x = point[0]; } else if (i == last && axes.findIndex('position', 'right') == -1) { x = point[0] - bbox.width; } } if (position == 'top') { y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2); } else { y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2); } textLabel.setAttributes({ hidden: false, x: x, y: y }, true); // Skip label if there isn't available minimum space if (i != 0 && (me.intersect(textLabel, prevLabel) || me.intersect(textLabel, firstLabel))) { textLabel.hide(true); continue; } prevLabel = textLabel; } return maxHeight; }, drawVerticalLabels: function() { var me = this, inflections = me.inflections, position = me.position, ln = inflections.length, labels = me.labels, maxWidth = 0, max = Math.max, floor = Math.floor, ceil = Math.ceil, axes = me.chart.axes, gutterY = me.chart.maxGutter[1], ubbox, bbox, point, prevLabel, projectedWidth = 0, textLabel, attr, textRight, text, label, last, x, y, i; last = ln; for (i = 0; i < last; i++) { point = inflections[i]; text = me.label.renderer(labels[i]); textLabel = me.getOrCreateLabel(i, text); bbox = textLabel._bbox; maxWidth = max(maxWidth, bbox.width + me.dashSize + me.label.padding); y = point[1]; if (gutterY < bbox.height / 2) { if (i == last - 1 && axes.findIndex('position', 'top') == -1) { y = me.y - me.length + ceil(bbox.height / 2); } else if (i == 0 && axes.findIndex('position', 'bottom') == -1) { y = me.y - floor(bbox.height / 2); } } if (position == 'left') { x = point[0] - bbox.width - me.dashSize - me.label.padding - 2; } else { x = point[0] + me.dashSize + me.label.padding + 2; } textLabel.setAttributes(Ext.apply({ hidden: false, x: x, y: y }, me.label), true); // Skip label if there isn't available minimum space if (i != 0 && me.intersect(textLabel, prevLabel)) { textLabel.hide(true); continue; } prevLabel = textLabel; } return maxWidth; }, /** * Renders the labels in the axes. */ drawLabel: function() { var me = this, position = me.position, labelGroup = me.labelGroup, inflections = me.inflections, maxWidth = 0, maxHeight = 0, ln, i; if (position == 'left' || position == 'right') { maxWidth = me.drawVerticalLabels(); } else { maxHeight = me.drawHorizontalLabels(); } // Hide unused bars ln = labelGroup.getCount(); i = inflections.length; for (; i < ln; i++) { labelGroup.getAt(i).hide(true); } me.bbox = {}; Ext.apply(me.bbox, me.axisBBox); me.bbox.height = maxHeight; me.bbox.width = maxWidth; if (Ext.isString(me.title)) { me.drawTitle(maxWidth, maxHeight); } }, // @private creates the elipsis for the text. elipsis: function(sprite, text, desiredWidth, minWidth, center) { var bbox, x; if (desiredWidth < minWidth) { sprite.hide(true); return false; } while (text.length > 4) { text = text.substr(0, text.length - 4) + "..."; sprite.setAttributes({ text: text }, true); bbox = sprite.getBBox(); if (bbox.width < desiredWidth) { if (typeof center == 'number') { sprite.setAttributes({ x: Math.floor(center - (bbox.width / 2)) }, true); } break; } } return true; }, /** * Updates the {@link #title} of this axis. * @param {String} title */ setTitle: function(title) { this.title = title; this.drawLabel(); }, // @private draws the title for the axis. drawTitle: function(maxWidth, maxHeight) { var me = this, position = me.position, surface = me.chart.surface, displaySprite = me.displaySprite, title = me.title, rotate = (position == 'left' || position == 'right'), x = me.x, y = me.y, base, bbox, pad; if (displaySprite) { displaySprite.setAttributes({text: title}, true); } else { base = { type: 'text', x: 0, y: 0, text: title }; displaySprite = me.displaySprite = surface.add(Ext.apply(base, me.axisTitleStyle, me.labelTitle)); surface.renderItem(displaySprite); } bbox = displaySprite.getBBox(); pad = me.dashSize + me.label.padding; if (rotate) { y -= ((me.length / 2) - (bbox.height / 2)); if (position == 'left') { x -= (maxWidth + pad + (bbox.width / 2)); } else { x += (maxWidth + pad + bbox.width - (bbox.width / 2)); } me.bbox.width += bbox.width + 10; } else { x += (me.length / 2) - (bbox.width * 0.5); if (position == 'top') { y -= (maxHeight + pad + (bbox.height * 0.3)); } else { y += (maxHeight + pad + (bbox.height * 0.8)); } me.bbox.height += bbox.height + 10; } displaySprite.setAttributes({ translate: { x: x, y: y } }, true); } });