merge master
authorDan Vanderkam <dan@dygraphs.com>
Tue, 5 Apr 2011 14:35:42 +0000 (10:35 -0400)
committerDan Vanderkam <dan@dygraphs.com>
Tue, 5 Apr 2011 14:35:42 +0000 (10:35 -0400)
docs/index.html
dygraph-canvas.js
dygraph.js
tests/border.html
tests/century-scale.html
tests/demo.html
tests/noise.html
tests/reverse-y-axis.html
tests/styled-chart-labels.html [new file with mode: 0644]

index 060f31a..1d0569d 100644 (file)
@@ -99,8 +99,6 @@
 
       <p style="font-size:0.8em">(Mouse over to highlight individual values. Click and drag to zoom. Double-click to zoom back out. Change the number and hit enter to adjust the averaging period.)</p>
 
-      <div id="title" style="width:800px; margin: 1em;text-align:center; font-weight: bold; font-size: 125%;">Temperatures in New York vs. San Francisco</div>
-
       <div id="demodiv" style="width:800px; height:320px;"></div>
       <script type="text/javascript">
         g = new Dygraph(
             rollPeriod: 14,
             showRoller: true,
             customBars: true,
-            yAxisLabelWidth: 30
+            title: 'Daily Temperatures in New York vs. San Francisco',
+            ylabel: 'Temperature (F)',
+            legend: 'always',
+            labelsDivStyles: { 'textAlign': 'right' }
           }
         );
       </script>
         <li>Plots time series without using an external server or Flash</li>
         <li>Works in Internet Explorer (using excanvas)</li>
         <li>Lightweight (69kb) and responsive</li>
-        <li>Displays values on mouseover (this makes it easily discoverable)</li>
+        <li>Displays values on mouseover, making interaction easily discoverable</li>
         <li>Supports error bands around data series</li>
         <li>Interactive zoom</li>
         <li>Displays Annotations on the chart</li>
index 2f8821e..522629c 100644 (file)
@@ -7,6 +7,24 @@
  * - grid overlays
  * - error bars
  * - dygraphs attribute system
+ *
+ * High level overview of classes:
+ *
+ * - DygraphLayout
+ *     This contains all the data to be charted.
+ *     It uses data coordinates, but also records the chart range (in data
+ *     coordinates) and hence is able to calculate percentage positions ('In
+ *     this view, Point A lies 25% down the x-axis.')
+ *     Two things that it does not do are:
+ *     1. Record pixel coordinates for anything.
+ *     2. (oddly) determine anything about the layout of chart elements.
+ *     The naming is a vestige of Dygraph's original PlotKit roots.
+ *
+ * - DygraphCanvasRenderer
+ *     This class determines the charting area (in pixel coordinates), maps the
+ *     percentage coordinates in the DygraphLayout to pixels and draws them.
+ *     It's also responsible for creating chart DOM elements, i.e. annotations,
+ *     tick mark labels, the title and the x/y-axis labels.
  */
 
 /**
@@ -301,6 +319,7 @@ DygraphCanvasRenderer = function(dygraph, element, layout, options) {
   this.xlabels = new Array();
   this.ylabels = new Array();
   this.annotations = new Array();
+  this.chartLabels = {};
 
   // TODO(danvk): consider all axes in this computation.
   this.area = {
@@ -321,6 +340,21 @@ DygraphCanvasRenderer = function(dygraph, element, layout, options) {
                         "to use " + this.dygraph_.numAxes() + ")");
   }
 
+  // Add space for chart labels: title, xlabel and ylabel.
+  if (this.attr_('title')) {
+    this.area.h -= this.attr_('titleHeight');
+    this.area.y += this.attr_('titleHeight');
+  }
+  if (this.attr_('xlabel')) {
+    this.area.h -= this.attr_('xLabelHeight');
+  }
+  if (this.attr_('ylabel')) {
+    // It would make sense to shift the chart here to make room for the y-axis
+    // label, but the default yAxisLabelWidth is large enough that this results
+    // in overly-padded charts. The y-axis label should fit fine. If it
+    // doesn't, the yAxisLabelWidth option can be increased.
+  }
+
   this.container.style.position = "relative";
   this.container.style.width = this.width + "px";
 
@@ -374,9 +408,15 @@ DygraphCanvasRenderer.prototype.clear = function() {
     var el = this.annotations[i];
     if (el.parentNode) el.parentNode.removeChild(el);
   }
+  for (var k in this.chartLabels) {
+    if (!this.chartLabels.hasOwnProperty(k)) continue;
+    var el = this.chartLabels[k];
+    if (el.parentNode) el.parentNode.removeChild(el);
+  }
   this.xlabels = new Array();
   this.ylabels = new Array();
   this.annotations = new Array();
+  this.chartLabels = {};
 };
 
 
@@ -452,6 +492,7 @@ DygraphCanvasRenderer.prototype.render = function() {
   // Do the ordinary rendering, as before
   this._renderLineChart();
   this._renderAxis();
+  this._renderChartLabels(); 
   this._renderAnnotations();
 };
 
@@ -518,7 +559,7 @@ DygraphCanvasRenderer.prototype._renderAxis = function() {
           label.style.top = top + "px";
         }
         if (tick[0] == 0) {
-          label.style.left = "0px";
+          label.style.left = (this.area.x - this.options.yAxisLabelWidth - this.options.axisTickSize) + "px";
           label.style.textAlign = "right";
         } else if (tick[0] == 1) {
           label.style.left = (this.area.x + this.area.w +
@@ -575,7 +616,7 @@ DygraphCanvasRenderer.prototype._renderAxis = function() {
 
         var label = makeDiv(tick[1]);
         label.style.textAlign = "center";
-        label.style.bottom = "0px";
+        label.style.top = (y + this.options.axisTickSize) + 'px';
 
         var left = (x - this.options.axisLabelWidth/2);
         if (left + this.options.axisLabelWidth > this.width) {
@@ -605,6 +646,101 @@ DygraphCanvasRenderer.prototype._renderAxis = function() {
 };
 
 
+DygraphCanvasRenderer.prototype._renderChartLabels = function() {
+  // Generate divs for the chart title, xlabel and ylabel.
+  // Space for these divs has already been taken away from the charting area in
+  // the DygraphCanvasRenderer constructor.
+  if (this.attr_('title')) {
+    var div = document.createElement("div");
+    div.style.position = 'absolute';
+    div.style.top = '0px';
+    div.style.left = this.area.x + 'px';
+    div.style.width = this.area.w + 'px';
+    div.style.height = this.attr_('titleHeight') + 'px';
+    div.style.textAlign = 'center';
+    div.style.fontSize = (this.attr_('titleHeight') - 8) + 'px';
+    div.style.fontWeight = 'bold';
+    var class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-title';
+    class_div.innerHTML = this.attr_('title');
+    div.appendChild(class_div);
+    this.container.appendChild(div);
+    this.chartLabels.title = div;
+  }
+
+  if (this.attr_('xlabel')) {
+    var div = document.createElement("div");
+    div.style.position = 'absolute';
+    div.style.bottom = 0;  // TODO(danvk): this is lazy. Calculate style.top.
+    div.style.left = this.area.x + 'px';
+    div.style.width = this.area.w + 'px';
+    div.style.height = this.attr_('xLabelHeight') + 'px';
+    div.style.textAlign = 'center';
+    div.style.fontSize = (this.attr_('xLabelHeight') - 2) + 'px';
+
+    var class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-xlabel';
+    class_div.innerHTML = this.attr_('xlabel');
+    div.appendChild(class_div);
+    this.container.appendChild(div);
+    this.chartLabels.xlabel = div;
+  }
+
+  if (this.attr_('ylabel')) {
+    var box = {
+      left: 0,
+      top: this.area.y,
+      width: this.attr_('yLabelWidth'),
+      height: this.area.h
+    };
+    // TODO(danvk): is this outer div actually necessary?
+    var div = document.createElement("div");
+    div.style.position = 'absolute';
+    div.style.left = box.left;
+    div.style.top = box.top + 'px';
+    div.style.width = box.width + 'px';
+    div.style.height = box.height + 'px';
+    div.style.fontSize = (this.attr_('yLabelWidth') - 2) + 'px';
+
+    var inner_div = document.createElement("div");
+    inner_div.style.position = 'absolute';
+    inner_div.style.width = box.height + 'px';
+    inner_div.style.height = box.width + 'px';
+    inner_div.style.top = (box.height / 2 - box.width / 2) + 'px';
+    inner_div.style.left = (box.width / 2 - box.height / 2) + 'px';
+    inner_div.style.textAlign = 'center';
+
+    // CSS rotation is an HTML5 feature which is not standardized. Hence every
+    // browser has its own name for the CSS style.
+    inner_div.style.transform = 'rotate(-90deg)';        // HTML5
+    inner_div.style.WebkitTransform = 'rotate(-90deg)';  // Safari/Chrome
+    inner_div.style.MozTransform = 'rotate(-90deg)';     // Firefox
+    inner_div.style.OTransform = 'rotate(-90deg)';       // Opera
+    inner_div.style.msTransform = 'rotate(-90deg)';      // IE9
+
+    if (typeof(document.documentMode) !== 'undefined' &&
+        document.documentMode < 9) {
+      // We're dealing w/ an old version of IE, so we have to rotate the text
+      // using a BasicImage transform. This uses a different origin of rotation
+      // than HTML5 rotation (top left of div vs. its center).
+      inner_div.style.filter =
+       'progid:DXImageTransform.Microsoft.BasicImage(rotation=3)';
+      inner_div.style.left = '0px';
+      inner_div.style.top = '0px';
+    }
+
+    var class_div = document.createElement("div");
+    class_div.className = 'dygraph-label dygraph-ylabel';
+    class_div.innerHTML = this.attr_('ylabel');
+
+    inner_div.appendChild(class_div);
+    div.appendChild(inner_div);
+    this.container.appendChild(div);
+    this.chartLabels.ylabel = div;
+  }
+};
+
+
 DygraphCanvasRenderer.prototype._renderAnnotations = function() {
   var annotationStyle = {
     "position": "absolute",
index 00af051..fe677f2 100644 (file)
@@ -193,6 +193,11 @@ Dygraph.DEFAULT_ATTRS = {
   stepPlot: false,
   avoidMinZero: false,
 
+  // Sizes of the various chart labels.
+  titleHeight: 28,
+  xLabelHeight: 18,
+  yLabelWidth: 18,
+
   interactionModel: null  // will be set to Dygraph.defaultInteractionModel.
 };
 
@@ -959,8 +964,9 @@ Dygraph.prototype.createStatusMessage_ = function() {
 };
 
 /**
- * Position the labels div so that its right edge is flush with the right edge
- * of the charting area.
+ * Position the labels div so that:
+ * - its right edge is flush with the right edge of the charting area
+ * - its top edge is flush with the top edge of the charting area
  */
 Dygraph.prototype.positionLabelsDiv_ = function() {
   // Don't touch a user-specified labelsDiv.
@@ -969,6 +975,7 @@ Dygraph.prototype.positionLabelsDiv_ = function() {
   var area = this.plotter_.area;
   var div = this.attr_("labelsDiv");
   div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
+  div.style.top = area.y + "px";
 };
 
 /**
@@ -986,10 +993,11 @@ Dygraph.prototype.createRollInterface_ = function() {
 
   var display = this.attr_('showRoller') ? 'block' : 'none';
 
+  var area = this.plotter_.area;
   var textAttr = { "position": "absolute",
                    "zIndex": 10,
-                   "top": (this.plotter_.area.h - 25) + "px",
-                   "left": (this.plotter_.area.x + 1) + "px",
+                   "top": (area.y + area.h - 25) + "px",
+                   "left": (area.x + 1) + "px",
                    "display": display
                   };
   this.roller_.size = "2";
@@ -4164,7 +4172,7 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
   "sigma": {
     "default": "2.0",
     "labels": ["Error Bars"],
-    "type": "integer",
+    "type": "float",
     "description": "When errorBars is set, shade this many standard deviations above/below each point."
   },
   "customBars": {
@@ -4198,6 +4206,42 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
     "default": "null",
     "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
   },
+  "title": {
+    "labels": ["Chart labels"],
+    "type": "string",
+    "default": "null",
+    "description": "Text to display above the chart. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-title' classes."
+  },
+  "titleHeight": {
+    "default": "18",
+    "labels": ["Chart labels"],
+    "type": "integer",
+    "description": "Height of the chart title, in pixels. This also controls the default font size of the title. If you style the title on your own, this controls how much space is set aside above the chart for the title's div."
+  },
+  "xlabel": {
+    "labels": ["Chart labels"],
+    "type": "string",
+    "default": "null",
+    "description": "Text to display below the chart's x-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-xlabel' classes."
+  },
+  "xLabelHeight": {
+    "labels": ["Chart labels"],
+    "type": "integer",
+    "default": "18",
+    "description": "Height of the x-axis label, in pixels. This also controls the default font size of the x-axis label. If you style the label on your own, this controls how much space is set aside below the chart for the x-axis label's div."
+  },
+  "ylabel": {
+    "labels": ["Chart labels"],
+    "type": "string",
+    "default": "null",
+    "description": "Text to display to the left of the chart's y-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-ylabel' classes. The text will be rotated 90 degrees by default, so CSS rules may behave in unintuitive ways. No additional space is set aside for a y-axis label. If you need more space, increase the width of the y-axis tick labels using the yAxisLabelWidth option. If you need a wider div for the y-axis label, either style it that way with CSS (but remember that it's rotated, so width is controlled by the 'height' property) or set the yLabelWidth option."
+  },
+  "yLabelWidth": {
+    "labels": ["Chart labels"],
+    "type": "integer",
+    "default": "18",
+    "description": "Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."
+  },
   "isZoomedIgnoreProgrammaticZoom" : {
     "default": "false",
     "labels": ["Zooming"],
@@ -4208,7 +4252,8 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
 ;  // </JSON>
 // NOTE: in addition to parsing as JS, this snippet is expected to be valid
 // JSON. This assumption cannot be checked in JS, but it will be checked when
-// documentation is generated by the generate-documentation.py script.
+// documentation is generated by the generate-documentation.py script. For the
+// most part, this just means that you should always use double quotes.
 
 // Do a quick sanity check on the options reference.
 (function() {
@@ -4217,6 +4262,7 @@ Dygraph.OPTIONS_REFERENCE =  // <JSON>
   var valid_cats = [ 
    'Annotations',
    'Axis display',
+   'Chart labels',
    'CSV parsing',
    'Callbacks',
    'Data Line display',
index 9079448..cbf5f9e 100644 (file)
     <script type="text/javascript">
     new Dygraph(document.getElementById('bordered'), data,
     {
-      labelsDivStyles: { border: '1px solid black' }
+      labelsDivStyles: { border: '1px solid black' },
+      title: 'Chart Title',
+      xlabel: 'Date',
+      ylabel: 'Temperature (F)'
     });
     </script>
   </body>
index ae4eaef..271a652 100644 (file)
@@ -36,7 +36,6 @@
                          "rgb(255,100,100)",
                          "#00DD55",
                          "rgba(50,50,200,0.4)"],
-                padding: {left: 40, right: 30, top: 15, bottom: 15},
                 width: 480,
                 height: 320
               }
index 2a2ea20..5859a23 100644 (file)
@@ -36,9 +36,7 @@
                 }
                 return r;
               },
-              null,
               {
-                rollPeriod: 1,
                 labelsDiv: document.getElementById('status'),
                 labelsSeparateLines: true,
                 labelsKMB: true,
                          "rgb(255,100,100)",
                          "#00DD55",
                          "rgba(50,50,200,0.4)"],
-                padding: {left: 40, right: 30, top: 15, bottom: 15},
-                width: 480,
-                height: 320
+                width: 640,
+                height: 480,
+                title: 'Interesting Shapes',
+                xlabel: 'Date',
+                ylabel: 'Count'
               }
           );
     </script>
index 902de67..6c5b566 100644 (file)
@@ -13,9 +13,8 @@
     <script type="text/javascript" src="data.js"></script>
   </head>
   <body>
-    <p>7-day rollup:</p>
     <div id="div_g" style="width:600px; height:300px;"></div>
-    <p>14-Day Rollup:</p>
+    <br/>
     <div id="div_g30" style="width:600px; height:300px;"></div>
 
     <script type="text/javascript">
             NoisyData, {
               rollPeriod: 7,
               errorBars: true,
-              legend: 'always'
+              legend: 'always',
+              ylabel: 'Percent Error',
+              yAxisLabelWidth: 25,
+              title: 'Error Rates over Time (7-day rollup)'
             }
           );
       g30 = new Dygraph(
               errorBars: true,
               legend: 'always',
               labelsDivStyles: {
-                'text-align': 'right',
+                'textAlign': 'right',
               },
-              labelsDivWidth: 170
+              labelsDivWidth: 170,
+              ylabel: 'Percent Error',
+              yAxisLabelWidth: 25,
+              title: 'Error Rates over Time (14-day rollup)'
             }
           );
     </script>
index 2328a41..5d0869e 100644 (file)
@@ -37,7 +37,6 @@
                 }
                 return r;
               },
-              null,
               {
                 rollPeriod: 1,
                 labelsDiv: document.getElementById('status'),
@@ -47,7 +46,6 @@
                          "rgb(255,100,100)",
                          "#00DD55",
                          "rgba(50,50,200,0.4)"],
-                padding: {left: 40, right: 30, top: 15, bottom: 15},
                 width: 480,
                 height: 320,
                 valueRange: [3000, 0]  // Reverse y-axis
diff --git a/tests/styled-chart-labels.html b/tests/styled-chart-labels.html
new file mode 100644 (file)
index 0000000..b7af95e
--- /dev/null
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>two series</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../strftime/strftime-min.js"></script>
+    <script type="text/javascript" src="../rgbcolor/rgbcolor.js"></script>
+    <script type="text/javascript" src="../dygraph-canvas.js"></script>
+    <script type="text/javascript" src="../dygraph.js"></script>
+    <script type="text/javascript" src="data.js"></script>
+    <style type="text/css">
+    #div_g .dygraph-label {
+      /* This applies to the title, x-axis label and y-axis label */
+      font-family: Arial, Helvetica, sans-serif;
+    }
+    #div_g .dygraph-title {
+      /* This rule only applies to the chart title */
+      font-size: 24px;
+      text-shadow: gray 2px 2px 2px;  /* color, delta-x, delta-y, blur radius */
+    }
+    #div_g .dygraph-ylabel {
+      /* This rule only applies to the y-axis label */
+      font-size: 18px;
+      text-shadow: gray -2px 2px 2px;  /* (offsets are in a rotated frame) */
+    }
+    .chart {
+      border: 1px dashed black;
+      margin: 5px;
+      padding: 2px;
+    }
+    </style>
+  </head>
+  <body>
+    <p>In this chart, each chart label is styled independently. View source to
+    see how it works.</p>
+
+    <div class="chart" id="div_g" style="width:600px; height:300px;"></div>
+
+    <p>This version of the chart uses the default styles:</p>
+    <div class="chart" id="div_g2" style="width:600px; height:300px;"></div>
+
+    <script type="text/javascript">
+      g = new Dygraph(
+            document.getElementById("div_g"),
+            data, {
+              rollPeriod: 7,
+              legend: 'always',
+              title: 'High and Low Temperatures',
+              titleHeight: 32,
+              ylabel: 'Temperature (F)',
+              xlabel: 'Date (Ticks indicate the start of the indicated time period)',
+              labelsDivStyles: {
+                'text-align': 'right',
+                'background': 'none'
+              },
+              strokeWidth: 1.5
+            }
+          );
+
+      g2 = new Dygraph(
+            document.getElementById("div_g2"),
+            data, {
+              rollPeriod: 30,
+              legend: 'always',
+              title: 'High and Low Temperatures (30-day average)',
+              ylabel: 'Temperature (F)',
+              xlabel: 'Date (Ticks indicate the start of the indicated time period)',
+              labelsDivStyles: {
+                'text-align': 'right',
+                'background': 'none'
+              },
+              strokeWidth: 1.5
+            }
+          );
+    </script>
+  </body>
+</html>