X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/chart/axis/Axis.js diff --git a/src/chart/axis/Axis.js b/src/chart/axis/Axis.js new file mode 100644 index 00000000..326d710c --- /dev/null +++ b/src/chart/axis/Axis.js @@ -0,0 +1,762 @@ +/** + * @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); + } +}); \ No newline at end of file