Skip to content
Snippets Groups Projects
dygraph-combined-dev.js 401 KiB
Newer Older
  • Learn to ignore specific revisions
  • Laura Cappelli's avatar
    Laura Cappelli committed
    2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000
       YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
    
     If the 'fractions' option is set, the input should be of the form:
    
       Date,SeriesA,SeriesB,...
       YYYYMMDD,A1/B1,A2/B2,...
       YYYYMMDD,A1/B1,A2/B2,...
    
     And error bars will be calculated automatically using a binomial distribution.
    
     For further documentation and examples, see http://dygraphs.com/
    
     */
    
    // For "production" code, this gets set to false by uglifyjs.
    if (typeof(DEBUG) === 'undefined') DEBUG=true;
    
    var Dygraph = (function() {
    /*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */
    "use strict";
    
    /**
     * Creates an interactive, zoomable chart.
     *
     * @constructor
     * @param {div | String} div A div or the id of a div into which to construct
     * the chart.
     * @param {String | Function} file A file containing CSV data or a function
     * that returns this data. The most basic expected format for each line is
     * "YYYY/MM/DD,val1,val2,...". For more information, see
     * http://dygraphs.com/data.html.
     * @param {Object} attrs Various other attributes, e.g. errorBars determines
     * whether the input data contains error ranges. For a complete list of
     * options, see http://dygraphs.com/options.html.
     */
    var Dygraph = function(div, data, opts, opt_fourth_param) {
      // These have to go above the "Hack for IE" in __init__ since .ready() can be
      // called as soon as the constructor returns. Once support for OldIE is
      // dropped, this can go down with the rest of the initializers.
      this.is_initial_draw_ = true;
      this.readyFns_ = [];
    
      if (opt_fourth_param !== undefined) {
        // Old versions of dygraphs took in the series labels as a constructor
        // parameter. This doesn't make sense anymore, but it's easy to continue
        // to support this usage.
        console.warn("Using deprecated four-argument dygraph constructor");
        this.__old_init__(div, data, opts, opt_fourth_param);
      } else {
        this.__init__(div, data, opts);
      }
    };
    
    Dygraph.NAME = "Dygraph";
    Dygraph.VERSION = "1.1.1";
    Dygraph.__repr__ = function() {
      return "[" + Dygraph.NAME + " " + Dygraph.VERSION + "]";
    };
    
    /**
     * Returns information about the Dygraph class.
     */
    Dygraph.toString = function() {
      return Dygraph.__repr__();
    };
    
    // Various default values
    Dygraph.DEFAULT_ROLL_PERIOD = 1;
    Dygraph.DEFAULT_WIDTH = 480;
    Dygraph.DEFAULT_HEIGHT = 320;
    
    // For max 60 Hz. animation:
    Dygraph.ANIMATION_STEPS = 12;
    Dygraph.ANIMATION_DURATION = 200;
    
    // Label constants for the labelsKMB and labelsKMG2 options.
    // (i.e. '100000' -> '100K')
    Dygraph.KMB_LABELS = [ 'K', 'M', 'B', 'T', 'Q' ];
    Dygraph.KMG2_BIG_LABELS = [ 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ];
    Dygraph.KMG2_SMALL_LABELS = [ 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y' ];
    
    // These are defined before DEFAULT_ATTRS so that it can refer to them.
    /**
     * @private
     * Return a string version of a number. This respects the digitsAfterDecimal
     * and maxNumberWidth options.
     * @param {number} x The number to be formatted
     * @param {Dygraph} opts An options view
     */
    Dygraph.numberValueFormatter = function(x, opts) {
      var sigFigs = opts('sigFigs');
    
      if (sigFigs !== null) {
        // User has opted for a fixed number of significant figures.
        return Dygraph.floatFormat(x, sigFigs);
      }
    
      var digits = opts('digitsAfterDecimal');
      var maxNumberWidth = opts('maxNumberWidth');
    
      var kmb = opts('labelsKMB');
      var kmg2 = opts('labelsKMG2');
    
      var label;
    
      // switch to scientific notation if we underflow or overflow fixed display.
      if (x !== 0.0 &&
          (Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
           Math.abs(x) < Math.pow(10, -digits))) {
        label = x.toExponential(digits);
      } else {
        label = '' + Dygraph.round_(x, digits);
      }
    
      if (kmb || kmg2) {
        var k;
        var k_labels = [];
        var m_labels = [];
        if (kmb) {
          k = 1000;
          k_labels = Dygraph.KMB_LABELS;
        }
        if (kmg2) {
          if (kmb) console.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
          k = 1024;
          k_labels = Dygraph.KMG2_BIG_LABELS;
          m_labels = Dygraph.KMG2_SMALL_LABELS;
        }
    
        var absx = Math.abs(x);
        var n = Dygraph.pow(k, k_labels.length);
        for (var j = k_labels.length - 1; j >= 0; j--, n /= k) {
          if (absx >= n) {
            label = Dygraph.round_(x / n, digits) + k_labels[j];
            break;
          }
        }
        if (kmg2) {
          // TODO(danvk): clean up this logic. Why so different than kmb?
          var x_parts = String(x.toExponential()).split('e-');
          if (x_parts.length === 2 && x_parts[1] >= 3 && x_parts[1] <= 24) {
            if (x_parts[1] % 3 > 0) {
              label = Dygraph.round_(x_parts[0] /
                  Dygraph.pow(10, (x_parts[1] % 3)),
                  digits);
            } else {
              label = Number(x_parts[0]).toFixed(2);
            }
            label += m_labels[Math.floor(x_parts[1] / 3) - 1];
          }
        }
      }
    
      return label;
    };
    
    /**
     * variant for use as an axisLabelFormatter.
     * @private
     */
    Dygraph.numberAxisLabelFormatter = function(x, granularity, opts) {
      return Dygraph.numberValueFormatter.call(this, x, opts);
    };
    
    /**
     * @type {!Array.<string>}
     * @private
     * @constant
     */
    Dygraph.SHORT_MONTH_NAMES_ = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    
    
    /**
     * Convert a JS date to a string appropriate to display on an axis that
     * is displaying values at the stated granularity. This respects the 
     * labelsUTC option.
     * @param {Date} date The date to format
     * @param {number} granularity One of the Dygraph granularity constants
     * @param {Dygraph} opts An options view
     * @return {string} The date formatted as local time
     * @private
     */
    Dygraph.dateAxisLabelFormatter = function(date, granularity, opts) {
      var utc = opts('labelsUTC');
      var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal;
    
      var year = accessors.getFullYear(date),
          month = accessors.getMonth(date),
          day = accessors.getDate(date),
          hours = accessors.getHours(date),
          mins = accessors.getMinutes(date),
          secs = accessors.getSeconds(date),
          millis = accessors.getSeconds(date);
    
      if (granularity >= Dygraph.DECADAL) {
        return '' + year;
      } else if (granularity >= Dygraph.MONTHLY) {
        return Dygraph.SHORT_MONTH_NAMES_[month] + '&#160;' + year;
      } else {
        var frac = hours * 3600 + mins * 60 + secs + 1e-3 * millis;
        if (frac === 0 || granularity >= Dygraph.DAILY) {
          // e.g. '21 Jan' (%d%b)
          return Dygraph.zeropad(day) + '&#160;' + Dygraph.SHORT_MONTH_NAMES_[month];
        } else {
          return Dygraph.hmsString_(hours, mins, secs);
        }
      }
    };
    // alias in case anyone is referencing the old method.
    Dygraph.dateAxisFormatter = Dygraph.dateAxisLabelFormatter;
    
    /**
     * Return a string version of a JS date for a value label. This respects the 
     * labelsUTC option.
     * @param {Date} date The date to be formatted
     * @param {Dygraph} opts An options view
     * @private
     */
    Dygraph.dateValueFormatter = function(d, opts) {
      return Dygraph.dateString_(d, opts('labelsUTC'));
    };
    
    /**
     * Standard plotters. These may be used by clients.
     * Available plotters are:
     * - Dygraph.Plotters.linePlotter: draws central lines (most common)
     * - Dygraph.Plotters.errorPlotter: draws error bars
     * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
     *
     * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
     * This causes all the lines to be drawn over all the fills/error bars.
     */
    Dygraph.Plotters = DygraphCanvasRenderer._Plotters;
    
    
    // Default attribute values.
    Dygraph.DEFAULT_ATTRS = {
      highlightCircleSize: 3,
      highlightSeriesOpts: null,
      highlightSeriesBackgroundAlpha: 0.5,
    
      labelsDivWidth: 250,
      labelsDivStyles: {
        // TODO(danvk): move defaults from createStatusMessage_ here.
      },
      labelsSeparateLines: false,
      labelsShowZeroValues: true,
      labelsKMB: false,
      labelsKMG2: false,
      showLabelsOnHighlight: true,
    
      digitsAfterDecimal: 2,
      maxNumberWidth: 6,
      sigFigs: null,
    
      strokeWidth: 1.0,
      strokeBorderWidth: 0,
      strokeBorderColor: "white",
    
      axisTickSize: 3,
      axisLabelFontSize: 14,
      rightGap: 5,
    
      showRoller: false,
      xValueParser: Dygraph.dateParser,
    
      delimiter: ',',
    
      sigma: 2.0,
      errorBars: false,
      fractions: false,
      wilsonInterval: true,  // only relevant if fractions is true
      customBars: false,
      fillGraph: false,
      fillAlpha: 0.15,
      connectSeparatedPoints: false,
    
      stackedGraph: false,
      stackedGraphNaNFill: 'all',
      hideOverlayOnMouseOut: true,
    
      legend: 'onmouseover',
      stepPlot: false,
      avoidMinZero: false,
      xRangePad: 0,
      yRangePad: null,
      drawAxesAtZero: false,
    
      // Sizes of the various chart labels.
      titleHeight: 28,
      xLabelHeight: 18,
      yLabelWidth: 18,
    
      drawXAxis: true,
      drawYAxis: true,
      axisLineColor: "black",
      axisLineWidth: 0.3,
      gridLineWidth: 0.3,
      axisLabelColor: "black",
      axisLabelWidth: 50,
      drawYGrid: true,
      drawXGrid: true,
      gridLineColor: "rgb(128,128,128)",
    
      interactionModel: null,  // will be set to Dygraph.Interaction.defaultModel
      animatedZooms: false,  // (for now)
    
      // Range selector options
      showRangeSelector: false,
      rangeSelectorHeight: 40,
      rangeSelectorPlotStrokeColor: "#808FAB",
      rangeSelectorPlotFillColor: "#A7B1C4",
      showInRangeSelector: null,
    
      // The ordering here ensures that central lines always appear above any
      // fill bars/error bars.
      plotter: [
        Dygraph.Plotters.fillPlotter,
        Dygraph.Plotters.errorPlotter,
        Dygraph.Plotters.linePlotter
      ],
    
      plugins: [ ],
    
      // per-axis options
      axes: {
        x: {
          pixelsPerLabel: 70,
          axisLabelWidth: 60,
          axisLabelFormatter: Dygraph.dateAxisLabelFormatter,
          valueFormatter: Dygraph.dateValueFormatter,
          drawGrid: true,
          drawAxis: true,
          independentTicks: true,
          ticker: null  // will be set in dygraph-tickers.js
        },
        y: {
          axisLabelWidth: 50,
          pixelsPerLabel: 30,
          valueFormatter: Dygraph.numberValueFormatter,
          axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
          drawGrid: true,
          drawAxis: true,
          independentTicks: true,
          ticker: null  // will be set in dygraph-tickers.js
        },
        y2: {
          axisLabelWidth: 50,
          pixelsPerLabel: 30,
          valueFormatter: Dygraph.numberValueFormatter,
          axisLabelFormatter: Dygraph.numberAxisLabelFormatter,
          drawAxis: true,  // only applies when there are two axes of data.
          drawGrid: false,
          independentTicks: false,
          ticker: null  // will be set in dygraph-tickers.js
        }
      }
    };
    
    // Directions for panning and zooming. Use bit operations when combined
    // values are possible.
    Dygraph.HORIZONTAL = 1;
    Dygraph.VERTICAL = 2;
    
    // Installed plugins, in order of precedence (most-general to most-specific).
    // Plugins are installed after they are defined, in plugins/install.js.
    Dygraph.PLUGINS = [
    ];
    
    // Used for initializing annotation CSS rules only once.
    Dygraph.addedAnnotationCSS = false;
    
    Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
      // Labels is no longer a constructor parameter, since it's typically set
      // directly from the data source. It also conains a name for the x-axis,
      // which the previous constructor form did not.
      if (labels !== null) {
        var new_labels = ["Date"];
        for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
        Dygraph.update(attrs, { 'labels': new_labels });
      }
      this.__init__(div, file, attrs);
    };
    
    /**
     * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
     * and context &lt;canvas&gt; inside of it. See the constructor for details.
     * on the parameters.
     * @param {Element} div the Element to render the graph into.
     * @param {string | Function} file Source data
     * @param {Object} attrs Miscellaneous other options
     * @private
     */
    Dygraph.prototype.__init__ = function(div, file, attrs) {
      // Hack for IE: if we're using excanvas and the document hasn't finished
      // loading yet (and hence may not have initialized whatever it needs to
      // initialize), then keep calling this routine periodically until it has.
      if (/MSIE/.test(navigator.userAgent) && !window.opera &&
          typeof(G_vmlCanvasManager) != 'undefined' &&
          document.readyState != 'complete') {
        var self = this;
        setTimeout(function() { self.__init__(div, file, attrs); }, 100);
        return;
      }
    
      // Support two-argument constructor
      if (attrs === null || attrs === undefined) { attrs = {}; }
    
      attrs = Dygraph.mapLegacyOptions_(attrs);
    
      if (typeof(div) == 'string') {
        div = document.getElementById(div);
      }
    
      if (!div) {
        console.error("Constructing dygraph with a non-existent div!");
        return;
      }
    
      this.isUsingExcanvas_ = typeof(G_vmlCanvasManager) != 'undefined';
    
      // Copy the important bits into the object
      // TODO(danvk): most of these should just stay in the attrs_ dictionary.
      this.maindiv_ = div;
      this.file_ = file;
      this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
      this.previousVerticalX_ = -1;
      this.fractions_ = attrs.fractions || false;
      this.dateWindow_ = attrs.dateWindow || null;
    
      this.annotations_ = [];
    
      // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
      this.zoomed_x_ = false;
      this.zoomed_y_ = false;
    
      // Clear the div. This ensure that, if multiple dygraphs are passed the same
      // div, then only one will be drawn.
      div.innerHTML = "";
    
      // For historical reasons, the 'width' and 'height' options trump all CSS
      // rules _except_ for an explicit 'width' or 'height' on the div.
      // As an added convenience, if the div has zero height (like <div></div> does
      // without any styles), then we use a default height/width.
      if (div.style.width === '' && attrs.width) {
        div.style.width = attrs.width + "px";
      }
      if (div.style.height === '' && attrs.height) {
        div.style.height = attrs.height + "px";
      }
      if (div.style.height === '' && div.clientHeight === 0) {
        div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
        if (div.style.width === '') {
          div.style.width = Dygraph.DEFAULT_WIDTH + "px";
        }
      }
      // These will be zero if the dygraph's div is hidden. In that case,
      // use the user-specified attributes if present. If not, use zero
      // and assume the user will call resize to fix things later.
      this.width_ = div.clientWidth || attrs.width || 0;
      this.height_ = div.clientHeight || attrs.height || 0;
    
      // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
      if (attrs.stackedGraph) {
        attrs.fillGraph = true;
        // TODO(nikhilk): Add any other stackedGraph checks here.
      }
    
      // DEPRECATION WARNING: All option processing should be moved from
      // attrs_ and user_attrs_ to options_, which holds all this information.
      //
      // Dygraphs has many options, some of which interact with one another.
      // To keep track of everything, we maintain two sets of options:
      //
      //  this.user_attrs_   only options explicitly set by the user.
      //  this.attrs_        defaults, options derived from user_attrs_, data.
      //
      // Options are then accessed this.attr_('attr'), which first looks at
      // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
      // defaults without overriding behavior that the user specifically asks for.
      this.user_attrs_ = {};
      Dygraph.update(this.user_attrs_, attrs);
    
      // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
      this.attrs_ = {};
      Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS);
    
      this.boundaryIds_ = [];
      this.setIndexByName_ = {};
      this.datasetIndex_ = [];
    
      this.registeredEvents_ = [];
      this.eventListeners_ = {};
    
      this.attributes_ = new DygraphOptions(this);
    
      // Create the containing DIV and other interactive elements
      this.createInterface_();
    
      // Activate plugins.
      this.plugins_ = [];
      var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins'));
      for (var i = 0; i < plugins.length; i++) {
        // the plugins option may contain either plugin classes or instances.
        // Plugin instances contain an activate method.
        var Plugin = plugins[i];  // either a constructor or an instance.
        var pluginInstance;
        if (typeof(Plugin.activate) !== 'undefined') {
          pluginInstance = Plugin;
        } else {
          pluginInstance = new Plugin();
        }
    
        var pluginDict = {
          plugin: pluginInstance,
          events: {},
          options: {},
          pluginOptions: {}
        };
    
        var handlers = pluginInstance.activate(this);
        for (var eventName in handlers) {
          if (!handlers.hasOwnProperty(eventName)) continue;
          // TODO(danvk): validate eventName.
          pluginDict.events[eventName] = handlers[eventName];
        }
    
        this.plugins_.push(pluginDict);
      }
    
      // At this point, plugins can no longer register event handlers.
      // Construct a map from event -> ordered list of [callback, plugin].
      for (var i = 0; i < this.plugins_.length; i++) {
        var plugin_dict = this.plugins_[i];
        for (var eventName in plugin_dict.events) {
          if (!plugin_dict.events.hasOwnProperty(eventName)) continue;
          var callback = plugin_dict.events[eventName];
    
          var pair = [plugin_dict.plugin, callback];
          if (!(eventName in this.eventListeners_)) {
            this.eventListeners_[eventName] = [pair];
          } else {
            this.eventListeners_[eventName].push(pair);
          }
        }
      }
    
      this.createDragInterface_();
    
      this.start_();
    };
    
    /**
     * Triggers a cascade of events to the various plugins which are interested in them.
     * Returns true if the "default behavior" should be prevented, i.e. if one
     * of the event listeners called event.preventDefault().
     * @private
     */
    Dygraph.prototype.cascadeEvents_ = function(name, extra_props) {
      if (!(name in this.eventListeners_)) return false;
    
      // QUESTION: can we use objects & prototypes to speed this up?
      var e = {
        dygraph: this,
        cancelable: false,
        defaultPrevented: false,
        preventDefault: function() {
          if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event.";
          e.defaultPrevented = true;
        },
        propagationStopped: false,
        stopPropagation: function() {
          e.propagationStopped = true;
        }
      };
      Dygraph.update(e, extra_props);
    
      var callback_plugin_pairs = this.eventListeners_[name];
      if (callback_plugin_pairs) {
        for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) {
          var plugin = callback_plugin_pairs[i][0];
          var callback = callback_plugin_pairs[i][1];
          callback.call(plugin, e);
          if (e.propagationStopped) break;
        }
      }
      return e.defaultPrevented;
    };
    
    /**
     * Fetch a plugin instance of a particular class. Only for testing.
     * @private
     * @param {!Class} type The type of the plugin.
     * @return {Object} Instance of the plugin, or null if there is none.
     */
    Dygraph.prototype.getPluginInstance_ = function(type) {
      for (var i = 0; i < this.plugins_.length; i++) {
        var p = this.plugins_[i];
        if (p.plugin instanceof type) {
          return p.plugin;
        }
      }
      return null;
    };
    
    /**
     * Returns the zoomed status of the chart for one or both axes.
     *
     * Axis is an optional parameter. Can be set to 'x' or 'y'.
     *
     * The zoomed status for an axis is set whenever a user zooms using the mouse
     * or when the dateWindow or valueRange are updated (unless the
     * isZoomedIgnoreProgrammaticZoom option is also specified).
     */
    Dygraph.prototype.isZoomed = function(axis) {
      if (axis === null || axis === undefined) {
        return this.zoomed_x_ || this.zoomed_y_;
      }
      if (axis === 'x') return this.zoomed_x_;
      if (axis === 'y') return this.zoomed_y_;
      throw "axis parameter is [" + axis + "] must be null, 'x' or 'y'.";
    };
    
    /**
     * Returns information about the Dygraph object, including its containing ID.
     */
    Dygraph.prototype.toString = function() {
      var maindiv = this.maindiv_;
      var id = (maindiv && maindiv.id) ? maindiv.id : maindiv;
      return "[Dygraph " + id + "]";
    };
    
    /**
     * @private
     * Returns the value of an option. This may be set by the user (either in the
     * constructor or by calling updateOptions) or by dygraphs, and may be set to a
     * per-series value.
     * @param {string} name The name of the option, e.g. 'rollPeriod'.
     * @param {string} [seriesName] The name of the series to which the option
     * will be applied. If no per-series value of this option is available, then
     * the global value is returned. This is optional.
     * @return { ... } The value of the option.
     */
    Dygraph.prototype.attr_ = function(name, seriesName) {
      if (DEBUG) {
        if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
          console.error('Must include options reference JS for testing');
        } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
          console.error('Dygraphs is using property ' + name + ', which has no ' +
                        'entry in the Dygraphs.OPTIONS_REFERENCE listing.');
          // Only log this error once.
          Dygraph.OPTIONS_REFERENCE[name] = true;
        }
      }
      return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name);
    };
    
    /**
     * Returns the current value for an option, as set in the constructor or via
     * updateOptions. You may pass in an (optional) series name to get per-series
     * values for the option.
     *
     * All values returned by this method should be considered immutable. If you
     * modify them, there is no guarantee that the changes will be honored or that
     * dygraphs will remain in a consistent state. If you want to modify an option,
     * use updateOptions() instead.
     *
     * @param {string} name The name of the option (e.g. 'strokeWidth')
     * @param {string=} opt_seriesName Series name to get per-series values.
     * @return {*} The value of the option.
     */
    Dygraph.prototype.getOption = function(name, opt_seriesName) {
      return this.attr_(name, opt_seriesName);
    };
    
    /**
     * Like getOption(), but specifically returns a number.
     * This is a convenience function for working with the Closure Compiler.
     * @param {string} name The name of the option (e.g. 'strokeWidth')
     * @param {string=} opt_seriesName Series name to get per-series values.
     * @return {number} The value of the option.
     * @private
     */
    Dygraph.prototype.getNumericOption = function(name, opt_seriesName) {
      return /** @type{number} */(this.getOption(name, opt_seriesName));
    };
    
    /**
     * Like getOption(), but specifically returns a string.
     * This is a convenience function for working with the Closure Compiler.
     * @param {string} name The name of the option (e.g. 'strokeWidth')
     * @param {string=} opt_seriesName Series name to get per-series values.
     * @return {string} The value of the option.
     * @private
     */
    Dygraph.prototype.getStringOption = function(name, opt_seriesName) {
      return /** @type{string} */(this.getOption(name, opt_seriesName));
    };
    
    /**
     * Like getOption(), but specifically returns a boolean.
     * This is a convenience function for working with the Closure Compiler.
     * @param {string} name The name of the option (e.g. 'strokeWidth')
     * @param {string=} opt_seriesName Series name to get per-series values.
     * @return {boolean} The value of the option.
     * @private
     */
    Dygraph.prototype.getBooleanOption = function(name, opt_seriesName) {
      return /** @type{boolean} */(this.getOption(name, opt_seriesName));
    };
    
    /**
     * Like getOption(), but specifically returns a function.
     * This is a convenience function for working with the Closure Compiler.
     * @param {string} name The name of the option (e.g. 'strokeWidth')
     * @param {string=} opt_seriesName Series name to get per-series values.
     * @return {function(...)} The value of the option.
     * @private
     */
    Dygraph.prototype.getFunctionOption = function(name, opt_seriesName) {
      return /** @type{function(...)} */(this.getOption(name, opt_seriesName));
    };
    
    Dygraph.prototype.getOptionForAxis = function(name, axis) {
      return this.attributes_.getForAxis(name, axis);
    };
    
    /**
     * @private
     * @param {string} axis The name of the axis (i.e. 'x', 'y' or 'y2')
     * @return { ... } A function mapping string -> option value
     */
    Dygraph.prototype.optionsViewForAxis_ = function(axis) {
      var self = this;
      return function(opt) {
        var axis_opts = self.user_attrs_.axes;
        if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
          return axis_opts[axis][opt];
        }
    
        // I don't like that this is in a second spot.
        if (axis === 'x' && opt === 'logscale') {
          // return the default value.
          // TODO(konigsberg): pull the default from a global default.
          return false;
        }
    
        // user-specified attributes always trump defaults, even if they're less
        // specific.
        if (typeof(self.user_attrs_[opt]) != 'undefined') {
          return self.user_attrs_[opt];
        }
    
        axis_opts = self.attrs_.axes;
        if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
          return axis_opts[axis][opt];
        }
        // check old-style axis options
        // TODO(danvk): add a deprecation warning if either of these match.
        if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) {
          return self.axes_[0][opt];
        } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) {
          return self.axes_[1][opt];
        }
        return self.attr_(opt);
      };
    };
    
    /**
     * Returns the current rolling period, as set by the user or an option.
     * @return {number} The number of points in the rolling window
     */
    Dygraph.prototype.rollPeriod = function() {
      return this.rollPeriod_;
    };
    
    /**
     * Returns the currently-visible x-range. This can be affected by zooming,
     * panning or a call to updateOptions.
     * Returns a two-element array: [left, right].
     * If the Dygraph has dates on the x-axis, these will be millis since epoch.
     */
    Dygraph.prototype.xAxisRange = function() {
      return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
    };
    
    /**
     * Returns the lower- and upper-bound x-axis values of the
     * data set.
     */
    Dygraph.prototype.xAxisExtremes = function() {
      var pad = this.getNumericOption('xRangePad') / this.plotter_.area.w;
      if (this.numRows() === 0) {
        return [0 - pad, 1 + pad];
      }
      var left = this.rawData_[0][0];
      var right = this.rawData_[this.rawData_.length - 1][0];
      if (pad) {
        // Must keep this in sync with dygraph-layout _evaluateLimits()
        var range = right - left;
        left -= range * pad;
        right += range * pad;
      }
      return [left, right];
    };
    
    /**
     * Returns the currently-visible y-range for an axis. This can be affected by
     * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
     * called with no arguments, returns the range of the first axis.
     * Returns a two-element array: [bottom, top].
     */
    Dygraph.prototype.yAxisRange = function(idx) {
      if (typeof(idx) == "undefined") idx = 0;
      if (idx < 0 || idx >= this.axes_.length) {
        return null;
      }
      var axis = this.axes_[idx];
      return [ axis.computedValueRange[0], axis.computedValueRange[1] ];
    };
    
    /**
     * Returns the currently-visible y-ranges for each axis. This can be affected by
     * zooming, panning, calls to updateOptions, etc.
     * Returns an array of [bottom, top] pairs, one for each y-axis.
     */
    Dygraph.prototype.yAxisRanges = function() {
      var ret = [];
      for (var i = 0; i < this.axes_.length; i++) {
        ret.push(this.yAxisRange(i));
      }
      return ret;
    };
    
    // TODO(danvk): use these functions throughout dygraphs.
    /**
     * Convert from data coordinates to canvas/div X/Y coordinates.
     * If specified, do this conversion for the coordinate system of a particular
     * axis. Uses the first axis by default.
     * Returns a two-element array: [X, Y]
     *
     * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
     * instead of toDomCoords(null, y, axis).
     */
    Dygraph.prototype.toDomCoords = function(x, y, axis) {
      return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
    };
    
    /**
     * Convert from data x coordinates to canvas/div X coordinate.
     * If specified, do this conversion for the coordinate system of a particular
     * axis.
     * Returns a single value or null if x is null.
     */
    Dygraph.prototype.toDomXCoord = function(x) {
      if (x === null) {
        return null;
      }
    
      var area = this.plotter_.area;
      var xRange = this.xAxisRange();
      return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
    };
    
    /**
     * Convert from data x coordinates to canvas/div Y coordinate and optional
     * axis. Uses the first axis by default.
     *
     * returns a single value or null if y is null.
     */
    Dygraph.prototype.toDomYCoord = function(y, axis) {
      var pct = this.toPercentYCoord(y, axis);
    
      if (pct === null) {
        return null;
      }
      var area = this.plotter_.area;
      return area.y + pct * area.h;
    };
    
    /**
     * Convert from canvas/div coords to data coordinates.
     * If specified, do this conversion for the coordinate system of a particular
     * axis. Uses the first axis by default.
     * Returns a two-element array: [X, Y].
     *
     * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
     * instead of toDataCoords(null, y, axis).
     */
    Dygraph.prototype.toDataCoords = function(x, y, axis) {
      return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
    };
    
    /**
     * Convert from canvas/div x coordinate to data coordinate.
     *
     * If x is null, this returns null.
     */
    Dygraph.prototype.toDataXCoord = function(x) {
      if (x === null) {
        return null;
      }
    
      var area = this.plotter_.area;
      var xRange = this.xAxisRange();
    
      if (!this.attributes_.getForAxis("logscale", 'x')) {
        return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
      } else {
        // TODO: remove duplicate code?
        // Computing the inverse of toDomCoord.
        var pct = (x - area.x) / area.w;
    
        // Computing the inverse of toPercentXCoord. The function was arrived at with
        // the following steps:
        //
        // Original calcuation:
        // pct = (log(x) - log(xRange[0])) / (log(xRange[1]) - log(xRange[0])));
        //
        // Multiply both sides by the right-side demoninator.
        // pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0])
        //
        // add log(xRange[0]) to both sides
        // log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])) = log(x);
        //
        // Swap both sides of the equation,
        // log(x) = log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0]))
        //
        // Use both sides as the exponent in 10^exp and we're done.
        // x = 10 ^ (log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])))
        var logr0 = Dygraph.log10(xRange[0]);
        var logr1 = Dygraph.log10(xRange[1]);
        var exponent = logr0 + (pct * (logr1 - logr0));
        var value = Math.pow(Dygraph.LOG_SCALE, exponent);
        return value;
      }
    };
    
    /**
     * Convert from canvas/div y coord to value.
     *
     * If y is null, this returns null.
     * if axis is null, this uses the first axis.
     */
    Dygraph.prototype.toDataYCoord = function(y, axis) {
      if (y === null) {
        return null;
      }
    
      var area = this.plotter_.area;
      var yRange = this.yAxisRange(axis);
    
      if (typeof(axis) == "undefined") axis = 0;
      if (!this.attributes_.getForAxis("logscale", axis)) {
        return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]);
      } else {
        // Computing the inverse of toDomCoord.
        var pct = (y - area.y) / area.h;
    
        // Computing the inverse of toPercentYCoord. The function was arrived at with
        // the following steps:
        //
        // Original calcuation:
        // pct = (log(yRange[1]) - log(y)) / (log(yRange[1]) - log(yRange[0]));
        //
        // Multiply both sides by the right-side demoninator.
        // pct * (log(yRange[1]) - log(yRange[0])) = log(yRange[1]) - log(y);
        //
        // subtract log(yRange[1]) from both sides.
        // (pct * (log(yRange[1]) - log(yRange[0]))) - log(yRange[1]) = -log(y);
        //
        // and multiply both sides by -1.
        // log(yRange[1]) - (pct * (logr1 - log(yRange[0])) = log(y);
        //
        // Swap both sides of the equation,
        // log(y) = log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0])));
        //
        // Use both sides as the exponent in 10^exp and we're done.
        // y = 10 ^ (log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0]))));
        var logr0 = Dygraph.log10(yRange[0]);
        var logr1 = Dygraph.log10(yRange[1]);
        var exponent = logr1 - (pct * (logr1 - logr0));
        var value = Math.pow(Dygraph.LOG_SCALE, exponent);
        return value;
      }
    };
    
    /**
     * Converts a y for an axis to a percentage from the top to the
     * bottom of the drawing area.
     *
     * If the coordinate represents a value visible on the canvas, then
     * the value will be between 0 and 1, where 0 is the top of the canvas.
     * However, this method will return values outside the range, as
     * values can fall outside the canvas.
     *
     * If y is null, this returns null.
     * if axis is null, this uses the first axis.
     *
     * @param {number} y The data y-coordinate.