Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / draw / Draw.js
index 8e7f743..4dabb72 100644 (file)
@@ -1,8 +1,22 @@
 /*
+
+This file is part of Ext JS 4
+
+Copyright (c) 2011 Sencha Inc
+
+Contact:  http://www.sencha.com/contact
+
+GNU General Public License Usage
+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.
+
+If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
+
+*/
+/**
  * @class Ext.draw.Draw
  * Base Drawing class.  Provides base drawing functions.
+ * @private
  */
-
 Ext.define('Ext.draw.Draw', {
     /* Begin Definitions */
 
@@ -96,12 +110,12 @@ Ext.define('Ext.draw.Draw', {
                     b && params.push(+b);
                 });
                 if (name == "m" && params.length > 2) {
-                    data.push([b].concat(params.splice(0, 2)));
+                    data.push([b].concat(Ext.Array.splice(params, 0, 2)));
                     name = "l";
                     b = (b == "m") ? "l" : "L";
                 }
                 while (params.length >= paramCounts[name]) {
-                    data.push([b].concat(params.splice(0, paramCounts[name])));
+                    data.push([b].concat(Ext.Array.splice(params, 0, paramCounts[name])));
                     if (!paramCounts[name]) {
                         break;
                     }
@@ -331,9 +345,9 @@ Ext.define('Ext.draw.Draw', {
                     points[i].shift();
                     point = points[i];
                     while (point.length) {
-                        points.splice(i++, 0, ["C"].concat(point.splice(0, 6)));
+                        Ext.Array.splice(points, i++, 0, ["C"].concat(Ext.Array.splice(point, 0, 6)));
                     }
-                    points.splice(i, 1);
+                    Ext.Array.erase(points, i, 1);
                     ln = points.length;
                 }
             seg = points[i];
@@ -357,15 +371,15 @@ Ext.define('Ext.draw.Draw', {
                     pp[i].shift();
                     var pi = pp[i];
                     while (pi.length) {
-                        pp.splice(i++, 0, ["C"].concat(pi.splice(0, 6)));
+                        Ext.Array.splice(pp, i++, 0, ["C"].concat(Ext.Array.splice(pi, 0, 6)));
                     }
-                    pp.splice(i, 1);
+                    Ext.Array.erase(pp, i, 1);
                     ii = Math.max(p.length, p2.length || 0);
                 }
             },
             fixM = function (path1, path2, a1, a2, i) {
                 if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") {
-                    path2.splice(i, 0, ["M", a2.x, a2.y]);
+                    Ext.Array.splice(path2, i, 0, ["M", a2.x, a2.y]);
                     a1.bx = 0;
                     a1.by = 0;
                     a1.x = path1[i][1];
@@ -766,27 +780,95 @@ Ext.define('Ext.draw.Draw', {
         };
     },
 
-    getAnchors: function (p1x, p1y, p2x, p2y, p3x, p3y, value) {
+    /**
+     * @private
+     *
+     * Calculates bezier curve control anchor points for a particular point in a path, with a
+     * smoothing curve applied. The smoothness of the curve is controlled by the 'value' parameter.
+     * Note that this algorithm assumes that the line being smoothed is normalized going from left
+     * to right; it makes special adjustments assuming this orientation.
+     *
+     * @param {Number} prevX X coordinate of the previous point in the path
+     * @param {Number} prevY Y coordinate of the previous point in the path
+     * @param {Number} curX X coordinate of the current point in the path
+     * @param {Number} curY Y coordinate of the current point in the path
+     * @param {Number} nextX X coordinate of the next point in the path
+     * @param {Number} nextY Y coordinate of the next point in the path
+     * @param {Number} value A value to control the smoothness of the curve; this is used to
+     * divide the distance between points, so a value of 2 corresponds to
+     * half the distance between points (a very smooth line) while higher values
+     * result in less smooth curves. Defaults to 4.
+     * @return {Object} Object containing x1, y1, x2, y2 bezier control anchor points; x1 and y1
+     * are the control point for the curve toward the previous path point, and
+     * x2 and y2 are the control point for the curve toward the next path point.
+     */
+    getAnchors: function (prevX, prevY, curX, curY, nextX, nextY, value) {
         value = value || 4;
-        var l = Math.min(Math.sqrt(Math.pow(p1x - p2x, 2) + Math.pow(p1y - p2y, 2)) / value, Math.sqrt(Math.pow(p3x - p2x, 2) + Math.pow(p3y - p2y, 2)) / value),
-            a = Math.atan((p2x - p1x) / Math.abs(p2y - p1y)),
-            b = Math.atan((p3x - p2x) / Math.abs(p2y - p3y)),
-            pi = Math.PI;
-        a = p1y < p2y ? pi - a : a;
-        b = p3y < p2y ? pi - b : b;
-        var alpha = pi / 2 - ((a + b) % (pi * 2)) / 2;
-        alpha > pi / 2 && (alpha -= pi);
-        var dx1 = l * Math.sin(alpha + a),
-            dy1 = l * Math.cos(alpha + a),
-            dx2 = l * Math.sin(alpha + b),
-            dy2 = l * Math.cos(alpha + b),
-            out = {
-                x1: p2x - dx1,
-                y1: p2y + dy1,
-                x2: p2x + dx2,
-                y2: p2y + dy2
-            };
-        return out;
+        var M = Math,
+            PI = M.PI,
+            halfPI = PI / 2,
+            abs = M.abs,
+            sin = M.sin,
+            cos = M.cos,
+            atan = M.atan,
+            control1Length, control2Length, control1Angle, control2Angle,
+            control1X, control1Y, control2X, control2Y, alpha;
+
+        // Find the length of each control anchor line, by dividing the horizontal distance
+        // between points by the value parameter.
+        control1Length = (curX - prevX) / value;
+        control2Length = (nextX - curX) / value;
+
+        // Determine the angle of each control anchor line. If the middle point is a vertical
+        // turnaround then we force it to a flat horizontal angle to prevent the curve from
+        // dipping above or below the middle point. Otherwise we use an angle that points
+        // toward the previous/next target point.
+        if ((curY >= prevY && curY >= nextY) || (curY <= prevY && curY <= nextY)) {
+            control1Angle = control2Angle = halfPI;
+        } else {
+            control1Angle = atan((curX - prevX) / abs(curY - prevY));
+            if (prevY < curY) {
+                control1Angle = PI - control1Angle;
+            }
+            control2Angle = atan((nextX - curX) / abs(curY - nextY));
+            if (nextY < curY) {
+                control2Angle = PI - control2Angle;
+            }
+        }
+
+        // Adjust the calculated angles so they point away from each other on the same line
+        alpha = halfPI - ((control1Angle + control2Angle) % (PI * 2)) / 2;
+        if (alpha > halfPI) {
+            alpha -= PI;
+        }
+        control1Angle += alpha;
+        control2Angle += alpha;
+
+        // Find the control anchor points from the angles and length
+        control1X = curX - control1Length * sin(control1Angle);
+        control1Y = curY + control1Length * cos(control1Angle);
+        control2X = curX + control2Length * sin(control2Angle);
+        control2Y = curY + control2Length * cos(control2Angle);
+
+        // One last adjustment, make sure that no control anchor point extends vertically past
+        // its target prev/next point, as that results in curves dipping above or below and
+        // bending back strangely. If we find this happening we keep the control angle but
+        // reduce the length of the control line so it stays within bounds.
+        if ((curY > prevY && control1Y < prevY) || (curY < prevY && control1Y > prevY)) {
+            control1X += abs(prevY - control1Y) * (control1X - curX) / (control1Y - curY);
+            control1Y = prevY;
+        }
+        if ((curY > nextY && control2Y < nextY) || (curY < nextY && control2Y > nextY)) {
+            control2X -= abs(nextY - control2Y) * (control2X - curX) / (control2Y - curY);
+            control2Y = nextY;
+        }
+        
+        return {
+            x1: control1X,
+            y1: control1Y,
+            x2: control2X,
+            y2: control2Y
+        };
     },
 
     /* Smoothing function for a path.  Converts a path into cubic beziers.  Value defines the divider of the distance between points.
@@ -857,7 +939,25 @@ Ext.define('Ext.draw.Draw', {
         };
     },
 
+    /**
+     * A utility method to deduce an appropriate tick configuration for the data set of given
+     * feature.
+     * 
+     * @param {Number/Date} from The minimum value in the data
+     * @param {Number/Date} to The maximum value in the data
+     * @param {Number} stepsMax The maximum number of ticks
+     * @return {Object} The calculated step and ends info; When `from` and `to` are Dates, refer to the
+     * return value of {@link #snapEndsByDate}. For numerical `from` and `to` the return value contains:
+     * @return {Number} return.from The result start value, which may be lower than the original start value
+     * @return {Number} return.to The result end value, which may be higher than the original end value
+     * @return {Number} return.power The calculate power.
+     * @return {Number} return.step The value size of each step
+     * @return {Number} return.steps The number of steps.
+     */
     snapEnds: function (from, to, stepsMax) {
+        if (Ext.isDate(from)) {
+            return this.snapEndsByDate(from, to, stepsMax);
+        }
         var step = (to - from) / stepsMax,
             level = Math.floor(Math.log(step) / Math.LN10) + 1,
             m = Math.pow(10, level),
@@ -895,6 +995,119 @@ Ext.define('Ext.draw.Draw', {
         };
     },
 
+    /**
+     * A utility method to deduce an appropriate tick configuration for the data set of given
+     * feature when data is Dates. Refer to {@link #snapEnds} for numeric data.
+     *
+     * @param {Date} from The minimum value in the data
+     * @param {Date} to The maximum value in the data
+     * @param {Number} stepsMax The maximum number of ticks
+     * @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values
+     * and will not be adjusted
+     * @return {Object} The calculated step and ends info; properties are:
+     * @return {Date} return.from The result start value, which may be lower than the original start value
+     * @return {Date} return.to The result end value, which may be higher than the original end value
+     * @return {Number} return.step The value size of each step
+     * @return {Number} return.steps The number of steps.
+     * NOTE: the steps may not divide the from/to range perfectly evenly;
+     * there may be a smaller distance between the last step and the end value than between prior
+     * steps, particularly when the `endsLocked` param is true. Therefore it is best to not use
+     * the `steps` result when finding the axis tick points, instead use the `step`, `to`, and
+     * `from` to find the correct point for each tick.
+     */
+    snapEndsByDate: function (from, to, stepsMax, lockEnds) {
+        var selectedStep = false, scales = [
+                [Ext.Date.MILLI, [1, 2, 3, 5, 10, 20, 30, 50, 100, 200, 300, 500]],
+                [Ext.Date.SECOND, [1, 2, 3, 5, 10, 15, 30]],
+                [Ext.Date.MINUTE, [1, 2, 3, 5, 10, 20, 30]],
+                [Ext.Date.HOUR, [1, 2, 3, 4, 6, 12]],
+                [Ext.Date.DAY, [1, 2, 3, 7, 14]],
+                [Ext.Date.MONTH, [1, 2, 3, 4, 6]]
+            ], j, yearDiff;
+
+        // Find the most desirable scale
+        Ext.each(scales, function(scale, i) {
+            for (j = 0; j < scale[1].length; j++) {
+                if (to < Ext.Date.add(from, scale[0], scale[1][j] * stepsMax)) {
+                    selectedStep = [scale[0], scale[1][j]];
+                    return false;
+                }
+            }
+        });
+        if (!selectedStep) {
+            yearDiff = this.snapEnds(from.getFullYear(), to.getFullYear() + 1, stepsMax, lockEnds);
+            selectedStep = [Date.YEAR, Math.round(yearDiff.step)];
+        }
+        return this.snapEndsByDateAndStep(from, to, selectedStep, lockEnds);
+    },
+
+
+    /**
+     * A utility method to deduce an appropriate tick configuration for the data set of given
+     * feature and specific step size.
+     * @param {Date} from The minimum value in the data
+     * @param {Date} to The maximum value in the data
+     * @param {Array} step An array with two components: The first is the unit of the step (day, month, year, etc).
+     * The second one is the number of units for the step (1, 2, etc.).
+     * @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values
+     * and will not be adjusted
+     * @return {Object} See the return value of {@link #snapEndsByDate}.
+     */
+    snapEndsByDateAndStep: function(from, to, step, lockEnds) {
+        var fromStat = [from.getFullYear(), from.getMonth(), from.getDate(),
+                from.getHours(), from.getMinutes(), from.getSeconds(), from.getMilliseconds()],
+            steps = 0, testFrom, testTo;
+        if (lockEnds) {
+            testFrom = from;
+        } else {
+            switch (step[0]) {
+                case Ext.Date.MILLI:
+                    testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+                            fromStat[4], fromStat[5], Math.floor(fromStat[6] / step[1]) * step[1]);
+                    break;
+                case Ext.Date.SECOND:
+                    testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+                            fromStat[4], Math.floor(fromStat[5] / step[1]) * step[1], 0);
+                    break;
+                case Ext.Date.MINUTE:
+                    testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+                            Math.floor(fromStat[4] / step[1]) * step[1], 0, 0);
+                    break;
+                case Ext.Date.HOUR:
+                    testFrom = new Date(fromStat[0], fromStat[1], fromStat[2],
+                            Math.floor(fromStat[3] / step[1]) * step[1], 0, 0, 0);
+                    break;
+                case Ext.Date.DAY:
+                    testFrom = new Date(fromStat[0], fromStat[1],
+                            Math.floor(fromStat[2] - 1 / step[1]) * step[1] + 1, 0, 0, 0, 0);
+                    break;
+                case Ext.Date.MONTH:
+                    testFrom = new Date(fromStat[0], Math.floor(fromStat[1] / step[1]) * step[1], 1, 0, 0, 0, 0);
+                    break;
+                default: // Ext.Date.YEAR
+                    testFrom = new Date(Math.floor(fromStat[0] / step[1]) * step[1], 0, 1, 0, 0, 0, 0);
+                    break;
+            }
+        }
+
+        testTo = testFrom;
+        // TODO(zhangbei) : We can do it better somehow...
+        while (testTo < to) {
+            testTo = Ext.Date.add(testTo, step[0], step[1]);
+            steps++;
+        }
+
+        if (lockEnds) {
+            testTo = to;
+        }
+        return {
+            from : +testFrom,
+            to : +testTo,
+            step : (testTo - testFrom) / steps,
+            steps : steps
+        };
+    },
+
     sorter: function (a, b) {
         return a.offset - b.offset;
     },
@@ -974,3 +1187,5 @@ Ext.define('Ext.draw.Draw', {
         }
     }
 });
+
+