Merge pull request #123 from kberg/custom-points
authorDan Vanderkam <dan@dygraphs.com>
Sat, 25 Feb 2012 19:38:36 +0000 (11:38 -0800)
committerDan Vanderkam <dan@dygraphs.com>
Sat, 25 Feb 2012 19:38:36 +0000 (11:38 -0800)
Custom drawing circles

auto_tests/tests/callback.js
dygraph-canvas.js
dygraph-options-reference.js
dygraph-utils.js
dygraph.js
tests/custom-circles.html [new file with mode: 0644]

index 1697e21..3431070 100644 (file)
@@ -13,42 +13,133 @@ CallbackTestCase.prototype.setUp = function() {
 CallbackTestCase.prototype.tearDown = function() {
 };
  
- var data = "X,a\,b,c\n" +
- "10,-1,1,2\n" +
- "11,0,3,1\n" +
- "12,1,4,2\n" +
- "13,0,2,3\n";
+var data = "X,a\,b,c\n" +
   "10,-1,1,2\n" +
   "11,0,3,1\n" +
   "12,1,4,2\n" +
   "13,0,2,3\n";
  
  
- /**
-  * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
-  * is properly called when the  first series is hidden (setVisibility = false) 
-  * 
-  */
- CallbackTestCase.prototype.testHighlightCallbackIsCalled = function() {
-   var h_row;
-   var h_pts;
-
-   var highlightCallback  =  function(e, x, pts, row) {
-         h_row = row;
-         h_pts = pts;
-   }; 
-
-   
-
-   var graph = document.getElementById("graph");
-   var g = new Dygraph(graph, data,
-       {
-         width: 100,
-         height : 100,
-         visibility: [false, true, true],
-         highlightCallback : highlightCallback,
-       });
-
-   DygraphOps.dispatchMouseMove(g, 13, 10);
-
-   //check correct row is returned
-   assertEquals(3, h_row);
-   //check there are only two points (because first series is hidden)
-   assertEquals(2, h_pts.length);
- };
+/**
+ * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
+ * is properly called when the first series is hidden (setVisibility = false) 
+ * 
+ */
+CallbackTestCase.prototype.testHighlightCallbackIsCalled = function() {
+  var h_row;
+  var h_pts;
+
+  var highlightCallback = function(e, x, pts, row) {
+    h_row = row;
+    h_pts = pts;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data,
+      {
+        width: 100,
+        height : 100,
+        visibility: [false, true, true],
+        highlightCallback : highlightCallback,
+      });
+
+  DygraphOps.dispatchMouseMove(g, 13, 10);
+
+  //check correct row is returned
+  assertEquals(3, h_row);
+  //check there are only two points (because first series is hidden)
+  assertEquals(2, h_pts.length);
+};
+
+
+/**
+ * Test that drawPointCallback isn't called when drawPoints is false
+ */
+CallbackTestCase.prototype.testDrawPointCallback_disabled = function() {
+  var called = false;
+
+  var callback = function() {
+    called = true;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, {
+      drawPointCallback : callback,
+    });
+
+  assertFalse(called);
+};
+
+/**
+ * Test that drawPointCallback is called when drawPoints is true
+ */
+CallbackTestCase.prototype.testDrawPointCallback_enabled = function() {
+  var called = false;
+
+  var callback = function() {
+    called = true;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, {
+      drawPoints : true,
+      drawPointCallback : callback
+    });
+
+  assertTrue(called);
+};
+
+/**
+ * Test that drawPointCallback is called when drawPoints is true
+ */
+CallbackTestCase.prototype.testDrawPointCallback_pointSize = function() {
+  var pointSize = 0;
+  var count = 0;
+
+  var callback = function(g, seriesName, canvasContext, cx, cy, color, pointSizeParam) {
+    pointSize = pointSizeParam;
+    count++;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data, {
+      drawPoints : true,
+      drawPointCallback : callback
+    });
+
+  assertEquals(1.5, pointSize);
+  assertEquals(12, count); // one call per data point.
+
+  var g = new Dygraph(graph, data, {
+      drawPoints : true,
+      drawPointCallback : callback,
+      pointSize : 8
+    });
+
+  assertEquals(8, pointSize);
+};
+
+/**
+ * This tests that when the function idxToRow_ returns the proper row and the onHiglightCallback
+ * is properly called when the first series is hidden (setVisibility = false) 
+ * 
+ */
+CallbackTestCase.prototype.testDrawHighlightPointCallbackIsCalled = function() {
+  var called = false;
+
+  var drawHighlightPointCallback  = function() {
+    called = true;
+  }; 
+
+  var graph = document.getElementById("graph");
+  var g = new Dygraph(graph, data,
+      {
+        width: 100,
+        height : 100,
+        drawHighlightPointCallback : drawHighlightPointCallback
+      });
+
+  assertFalse(called);
+  DygraphOps.dispatchMouseMove(g, 13, 10);
+  assertTrue(called);
+};
index 9c2e4da..10936de 100644 (file)
@@ -830,11 +830,17 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
     var strokeWidth = this.dygraph_.attr_("strokeWidth", setName);
 
     // setup graphics context
+    // TODO(konigsberg): This function has ctx and context. Clarify the difference.
     context.save();
     var pointSize = this.dygraph_.attr_("pointSize", setName);
     prevX = null;
     prevY = null;
     var drawPoints = this.dygraph_.attr_("drawPoints", setName);
+    var drawPointCallback = this.dygraph_.attr_("drawPointCallback", setName);
+    if (!drawPointCallback) {
+      drawPointCallback = Dygraph.Circles.DEFAULT;
+    }
+    var pointsOnLine = []; // Array of [canvasx, canvasy] pairs.
     var strokePattern = this.dygraph_.attr_("strokePattern", setName);
     if (!Dygraph.isArrayLike(strokePattern)) {
       strokePattern = null;
@@ -885,14 +891,19 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() {
         }
 
         if (drawPoints || isIsolated) {
-          ctx.beginPath();
-          ctx.fillStyle = color;
-          ctx.arc(point.canvasx, point.canvasy, pointSize,
-                  0, 2 * Math.PI, false);
-          ctx.fill();
+          pointsOnLine.push([point.canvasx, point.canvasy]);
         }
       }
     }
+    for (var idx = 0; idx < pointsOnLine.length; idx++) {
+      var cb = pointsOnLine[idx];
+      ctx.save();
+      drawPointCallback(
+          this.dygraph_, setName, ctx, cb[0], cb[1], color, pointSize);
+      ctx.restore();
+
+    }
+    firstIndexInSet = afterLastIndexInSet;
   }
 
   context.restore();
index 7f16cf2..2b45ed3 100644 (file)
@@ -41,7 +41,13 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "default": "false",
     "labels": ["Data Line display"],
     "type": "boolean",
-    "description": "Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart."
+    "description": "Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart. The small dot can be replaced with a custom rendering by supplying a drawPointCallback."
+  },
+  "drawPointCallback": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "function(g, seriesName, canvasContext, cx, cy, color, pointSize)",
+    "description": "Draw a custom item when drawPoints is enabled. Default is a small dot matching the series color."
   },
   "height": {
     "default": "320",
@@ -96,6 +102,12 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
       ["row", "???"]
     ]
   },
+  "drawHighlightPointCallback": {
+    "default": "null",
+    "labels": ["Data Line display"],
+    "type": "function(g, seriesName, canvasContext, cx, cy, color, pointSize)",
+    "description": "Draw a custom item when a point is highlighted. Default is a small dot matching the series color."
+  },
   "includeZero": {
     "default": "false",
     "labels": ["Axis display"],
index 5a21430..61aba43 100644 (file)
@@ -700,7 +700,9 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) {
     'clickCallback': true,
     'digitsAfterDecimal': true,
     'drawCallback': true,
+    'drawHighlightPointCallback': true,
     'drawPoints': true,
+    'drawPointCallback': true,
     'drawXGrid': true,
     'drawYGrid': true,
     'fillAlpha': true,
@@ -805,3 +807,110 @@ Dygraph.compareArrays = function(array1, array2) {
   }
   return true;
 };
+
+/**
+ * ctx: the canvas context
+ * sides: the number of sides in the shape.
+ * radius: the radius of the image.
+ * cx: center x coordate
+ * cy: center y coordinate
+ * rotationRadians: the shift of the initial angle, in radians.
+ * delta: the angle shift for each line. If missing, creates a regular
+ *   polygon.
+ */
+Dygraph.regularShape_ = function(
+    ctx, sides, radius, cx, cy, rotationRadians, delta) {
+  rotationRadians = rotationRadians ? rotationRadians : 0;
+  delta = delta ? delta : Math.PI * 2 / sides;
+
+  ctx.beginPath();
+  var first = true;
+  var initialAngle = rotationRadians;
+  var angle = initialAngle;
+
+  var computeCoordinates = function() {
+    var x = cx + (Math.sin(angle) * radius);
+    var y = cy + (-Math.cos(angle) * radius);
+    return [x, y]; 
+  };
+
+  var initialCoordinates = computeCoordinates();
+  var x = initialCoordinates[0];
+  var y = initialCoordinates[1];
+  ctx.moveTo(x, y);
+
+  for (var idx = 0; idx < sides; idx++) {
+    angle = (idx == sides - 1) ? initialAngle : (angle + delta);
+    var coords = computeCoordinates();
+    ctx.lineTo(coords[0], coords[1]);
+  }
+  ctx.stroke();
+  ctx.closePath();
+}
+
+Dygraph.shapeFunction_ = function(sides, rotationRadians, delta) {
+  return function(g, name, ctx, cx, cy, color, radius) {
+    ctx.lineWidth = 1;
+    ctx.strokeStyle = color;
+    Dygraph.regularShape_(ctx, sides, radius, cx, cy, rotationRadians, delta);
+  };
+};
+
+Dygraph.DrawPolygon_ = function(sides, rotationRadians, ctx, cx, cy, color, radius, delta) {
+  new Dygraph.RegularShape_(sides, rotationRadians, delta).draw(ctx, cx, cy, radius);
+}
+
+Dygraph.Circles = {
+  DEFAULT : function(g, name, ctx, canvasx, canvasy, color, radius) {
+    ctx.beginPath();
+    ctx.fillStyle = color;
+    ctx.arc(canvasx, canvasy, radius, 0, 2 * Math.PI, false);
+    ctx.fill();
+  },
+  TRIANGLE : Dygraph.shapeFunction_(3),
+  SQUARE : Dygraph.shapeFunction_(4, Math.PI / 4),
+  DIAMOND : Dygraph.shapeFunction_(4),
+  PENTAGON : Dygraph.shapeFunction_(5),
+  HEXAGON : Dygraph.shapeFunction_(6),
+  CIRCLE : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.beginPath();
+    ctx.strokeStyle = color;
+    ctx.arc(cx, cy, radius, 0, 2 * Math.PI, false);
+    ctx.stroke();
+  },
+  STAR : Dygraph.shapeFunction_(5, 0, 4 * Math.PI / 5),
+  PLUS : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.lineWidth = 1;
+    ctx.strokeStyle = color;
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy);
+    ctx.lineTo(cx - radius, cy);
+    ctx.closePath();
+    ctx.stroke();
+
+    ctx.beginPath();
+    ctx.moveTo(cx, cy + radius);
+    ctx.lineTo(cx, cy - radius);
+    ctx.closePath();
+
+    ctx.stroke();
+  },
+  EX : function(g, name, ctx, cx, cy, color, radius) {
+    ctx.lineWidth = 1;
+    ctx.strokeStyle = "black";
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy + radius);
+    ctx.lineTo(cx - radius, cy - radius);
+    ctx.closePath();
+    ctx.stroke();
+
+    ctx.beginPath();
+    ctx.moveTo(cx + radius, cy - radius);
+    ctx.lineTo(cx - radius, cy + radius);
+    ctx.closePath();
+
+    ctx.stroke();
+  }
+};
index 0748d26..680aa81 100644 (file)
@@ -1761,10 +1761,12 @@ Dygraph.prototype.updateSelection_ = function() {
       if (!Dygraph.isOK(pt.canvasy)) continue;
 
       var circleSize = this.attr_('highlightCircleSize', pt.name);
-      ctx.beginPath();
-      ctx.fillStyle = this.plotter_.colors[pt.name];
-      ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
-      ctx.fill();
+      var callback = this.attr_("drawHighlightPointCallback", pt.name);
+      if (!callback) {
+        callback = Dygraph.Circles.DEFAULT;
+      }
+      callback(this.g, pt.name, ctx, canvasx, pt.canvasy,
+          this.plotter_.colors[pt.name], circleSize);
     }
     ctx.restore();
 
diff --git a/tests/custom-circles.html b/tests/custom-circles.html
new file mode 100644 (file)
index 0000000..7a30ea2
--- /dev/null
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>Custom Circles</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <!--
+    For production (minified) code, use:
+    <script type="text/javascript" src="dygraph-combined.js"></script>
+    -->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+
+  </head>
+  <body>
+    <h2>Custom circles and hover circles</h2>
+    <div id="demodiv"></div>
+
+    <script type="text/javascript">
+      var smile = function(g, series, ctx, cx, cy, color, radius) {
+        mouthlessFace(g, series, ctx, cx, cy, color, radius);
+
+        ctx.fillStyle = "#000000";
+        ctx.beginPath();
+        ctx.arc(cx, cy, radius - 2, .3, Math.PI - .3, false);
+        ctx.stroke();
+      };
+
+      var frown = function(g, series, ctx, cx, cy, color, radius) {
+        mouthlessFace(g, series, ctx, cx, cy, color, radius);
+
+        ctx.fillStyle = "#000000";
+        ctx.beginPath();
+        ctx.arc(cx, cy + radius, radius - 2, Math.PI + .3, -.3, false);
+        ctx.stroke();
+      };
+
+      var mouthlessFace = function(g, series, ctx, cx, cy, color, radius) {
+        ctx.strokeStyle = "#000000";
+        ctx.fillStyle = "#FFFF00";
+        ctx.beginPath();
+        ctx.arc(cx, cy, radius, Math.PI * 2, false);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+
+        ctx.fillStyle = "#000000";
+        ctx.beginPath();
+        ctx.arc(cx - (radius / 3) , cy - (radius / 4), 1, Math.PI * 2, false);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+
+        ctx.beginPath();
+        ctx.arc(cx + (radius / 3) , cy - (radius / 4), 1, Math.PI * 2, false);
+        ctx.closePath();
+        ctx.stroke();
+        ctx.fill();
+      };
+
+      g = new Dygraph(
+          document.getElementById("demodiv"),
+          function() {
+
+            var r = "xval,default,triangle,square,diamond,pentagon,hexagon,circle,star,plus,ex,custom\n";
+            for (var i=1; i<=20; i++) {
+              r += i;
+              for (var j = 0; j < 11; j++) {
+                r += "," + j + (i / 3);
+              }
+              r += "\n";
+            }
+            return r;
+          },
+          {
+            drawPoints : true,
+            pointSize : 5,
+            highlightCircleSize : 8,
+            'default' : {
+              drawPointCallback : Dygraph.Circles.DEFAULT,
+              drawHighlightPointCallback : Dygraph.Circles.DEFAULT
+            },
+            'triangle' : {
+              drawPointCallback : Dygraph.Circles.TRIANGLE,
+              drawHighlightPointCallback : Dygraph.Circles.TRIANGLE
+            },
+            'square' : {
+              drawPointCallback : Dygraph.Circles.SQUARE,
+              drawHighlightPointCallback : Dygraph.Circles.SQUARE
+            },
+            'diamond' : {
+              drawPointCallback : Dygraph.Circles.DIAMOND,
+              drawHighlightPointCallback : Dygraph.Circles.DIAMOND
+            },
+            'pentagon' : {
+              drawPointCallback : Dygraph.Circles.PENTAGON,
+              drawHighlightPointCallback : Dygraph.Circles.PENTAGON
+            },
+            'hexagon' : {
+              drawPointCallback : Dygraph.Circles.HEXAGON,
+              drawHighlightPointCallback : Dygraph.Circles.HEXAGON
+            },
+            'circle' : {
+              drawPointCallback : Dygraph.Circles.CIRCLE,
+              drawHighlightPointCallback : Dygraph.Circles.CIRCLE
+            },
+            'star' : {
+              drawPointCallback : Dygraph.Circles.STAR,
+              drawHighlightPointCallback : Dygraph.Circles.STAR
+            },
+            'plus' : {
+              drawPointCallback : Dygraph.Circles.PLUS,
+              drawHighlightPointCallback : Dygraph.Circles.PLUS
+            },
+            'ex' : {
+              drawPointCallback : Dygraph.Circles.EX,
+              drawHighlightPointCallback : Dygraph.Circles.EX
+            },
+            'custom' : {
+              drawPointCallback : frown,
+              drawHighlightPointCallback : smile
+            }
+          }
+      );
+    </script>
+</body>
+</html>