]> arthur.barton.de Git - netdata.git/commitdiff
dygraphs now support smooth plotting on line charts; dygraphs can now be rendered...
authorCosta Tsaousis (ktsaou) <costa@tsaousis.gr>
Wed, 9 Dec 2015 01:12:24 +0000 (03:12 +0200)
committerCosta Tsaousis (ktsaou) <costa@tsaousis.gr>
Wed, 9 Dec 2015 01:12:24 +0000 (03:12 +0200)
web/Makefile.am
web/dashboard.css
web/dashboard.html
web/dashboard.js
web/lib/dygraph-smooth-plotter.js [new file with mode: 0644]
web/lib/dygraph-synchronizer.js [deleted file]

index 127c25e3b9afd1332c952eaa30f87d1d57eb7145..a41c15350201dfdb0176f170a8b108d2b3f691f8 100644 (file)
@@ -19,7 +19,7 @@ dist_web_DATA = \
 weblibdir=$(webdir)/lib
 dist_weblib_DATA = \
         lib/dygraph-combined.js \
-        lib/dygraph-synchronizer.js \
+        lib/dygraph-smooth-plotter.js \
         lib/jquery-1.11.3.min.js \
         lib/jquery.peity.min.js \
         lib/jquery.sparkline.min.js \
index a1f1fdee583d5eaeb242649819829c7d0ba41ce4..256bff3c2f18ab1c414f3d09b1afaf956bbdf983 100755 (executable)
@@ -63,6 +63,7 @@ html {
        -webkit-transition:opacity 0.2s ease;\r
        -moz-transition:opacity 0.2s ease;\r
        -o-transition:opacity 0.2s ease;\r
+       padding: 4px;\r
 }\r
 \r
 .dygraph-legend:hover {\r
@@ -71,6 +72,24 @@ html {
        opacity:0;\r
 }\r
 \r
+/* TEST\r
+.dygraph-legend {\r
+       position: absolute;\r
+       background: rgb(0, 0, 0) transparent !important;\r
+       background-color: rgba(0,0,0,0.6) !important;\r
+       filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000) !important;\r
+       -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)" !important;\r
+       color: white !important;\r
+       font: 10px arial, san serif !important;\r
+       text-align: left !important;\r
+       white-space: nowrap !important;\r
+       padding: 5px !important;\r
+       border: 1px solid white !important;\r
+       box-sizing: content-box !important;\r
+       z-index: 10000 !important;\r
+}\r
+*/\r
+\r
 .dygraph-title {\r
        text-indent: 56px;\r
        text-align: left;\r
index 4ec54c2935045edd11309ba803d0c841f0b332e0..0e38f9fc752bab4c86c249a0506a61159401451a 100755 (executable)
@@ -166,6 +166,31 @@ The fastest charting engine that can chart complete charts (not just sparklines)
 The charts are zoomable (drag their contents to pan, shift with mouse wheel to zoom-in or zoom-out, double click to reset it).
 <b>Netdata magic!</b> Realtime charts on your web page!
 <br/>
+Sparklines using dygraphs
+       <div data-netdata="system.processes"
+               data-chart-library="dygraph"
+               data-dygraph-theme="sparkline"
+               data-width="15%"
+               data-height="30"
+               data-after="-300"
+               ></div>
+       are also possible! This 
+       <div data-netdata="system.ipv4"
+               data-chart-library="dygraph"
+               data-dygraph-theme="sparkline"
+               data-width="15%"
+               data-height="30"
+               data-after="-300"
+               ></div>
+       is an area chart, while this
+       <div data-netdata="system.cpu"
+               data-chart-library="dygraph"
+               data-dygraph-theme="sparkline"
+               data-width="15%"
+               data-height="30"
+               data-after="-300"
+               ></div> is a stacked area chart!
+<br/>
 <div style="width: 24%; display: inline-block;">
        <div data-netdata="system.processes"
                data-chart-library="dygraph"
index 532d15ec0755a18c0081eab9fd83a4dd82715a0a..5702291d520c983a0281cedadb3eda33fa33b42a 100755 (executable)
        // default URLs for all the external files we need
        // make them RELATIVE so that the whole thing can also be
        // installed under a web server
-       NETDATA.jQuery          = NETDATA.serverDefault + 'lib/jquery-1.11.3.min.js';
-       NETDATA.peity_js        = NETDATA.serverDefault + 'lib/jquery.peity.min.js';
-       NETDATA.sparkline_js    = NETDATA.serverDefault + 'lib/jquery.sparkline.min.js';
-       NETDATA.dygraph_js      = NETDATA.serverDefault + 'lib/dygraph-combined.js';
-       NETDATA.raphael_js      = NETDATA.serverDefault + 'lib/raphael-min.js';
-       NETDATA.morris_js       = NETDATA.serverDefault + 'lib/morris.min.js';
-       NETDATA.morris_css      = NETDATA.serverDefault + 'css/morris.css';
-       NETDATA.dashboard_css   = NETDATA.serverDefault + 'dashboard.css';
-       NETDATA.google_js       = 'https://www.google.com/jsapi';
+       NETDATA.jQuery                  = NETDATA.serverDefault + 'lib/jquery-1.11.3.min.js';
+       NETDATA.peity_js                = NETDATA.serverDefault + 'lib/jquery.peity.min.js';
+       NETDATA.sparkline_js            = NETDATA.serverDefault + 'lib/jquery.sparkline.min.js';
+       NETDATA.dygraph_js              = NETDATA.serverDefault + 'lib/dygraph-combined.js';
+       NETDATA.dygraph_smooth_js   = NETDATA.serverDefault + 'lib/dygraph-smooth-plotter.js';
+       NETDATA.raphael_js              = NETDATA.serverDefault + 'lib/raphael-min.js';
+       NETDATA.morris_js               = NETDATA.serverDefault + 'lib/morris.min.js';
+       NETDATA.morris_css              = NETDATA.serverDefault + 'css/morris.css';
+       NETDATA.dashboard_css           = NETDATA.serverDefault + 'dashboard.css';
+       NETDATA.google_js               = 'https://www.google.com/jsapi';
 
        // these are the colors Google Charts are using
        // we have them here to attempt emulate their look and feel on the other chart libraries
                        },
 
                        resetChart: function() {
-                               if(NETDATA.globalPanAndZoom.isMaster(this)) {
-                                       NETDATA.globalPanAndZoom.clearMaster();
-                                       // it will call us back - no need to do
-                                       // anything more.
-                                       return;
-                               }
+                               NETDATA.globalPanAndZoom.clearMaster();
+                               this.follows_global = 0;
 
-                               //if(state.current.name != 'auto')
-                                       this.setMode('auto');
+                               this.clearSelection();
 
+                               this.setMode('auto');
                                this.current.force_before_ms = null;
                                this.current.force_after_ms = null;
                                this.current.last_autorefreshed = 0;
-                               this.follows_global = 0;
                                this.paused = false;
                                this.selected = false;
                                this.enabled = true;
                                // update the performance counters
                                var now = new Date().getTime();
                                
-                               // don't update last_update_ms if this chart is
+                               // don't update last_autorefreshed if this chart is
                                // forced to be updated with global PanAndZoom
                                if(NETDATA.globalPanAndZoom.isActive())
                                        this.current.last_autorefreshed = 0;
        // dygraph
 
        NETDATA.dygraph = {
+               smooth: false,
                state: null,
                sync: false,
                dont_sync_before: 0,
                if(NETDATA.options.debug.dygraph) state.log('dygraph.resetChart()');
 
                state.resetChart();
-               if(NETDATA.globalPanAndZoom.clearMaster());
        }
 
        NETDATA.dygraph.chartPanOrZoom = function(state, dygraph, after, before) {
                return true;
        }
 
+       NETDATA.dygraphSmoothInitialize = function(callback) {
+               $.ajax({
+                       url: NETDATA.dygraph_smooth_js,
+                       cache: true,
+                       dataType: "script"
+               })
+                       .done(function() {
+                               NETDATA.dygraph.smooth = true;
+                               smoothPlotter.smoothing = 0.3;
+                       })
+                       .always(function() {
+                               if(typeof callback == "function")
+                                       callback();
+                       })
+       };
+
        NETDATA.dygraphInitialize = function(callback) {
                if(typeof netdataNoDygraphs == 'undefined' || !netdataNoDygraphs) {
                        $.ajax({
                        })
                                .done(function() {
                                        NETDATA.registerChartLibrary('dygraph', NETDATA.dygraph_js);
+                                       NETDATA.dygraphSmoothInitialize(callback);
                                })
                                .fail(function() {
                                        NETDATA.error(100, NETDATA.dygraph_js);
-                               })
-                               .always(function() {
                                        if(typeof callback == "function")
                                                callback();
                                })
                if(NETDATA.options.debug.dygraph || state.debug) state.log('dygraphChartCreate()');
 
                var self = $(state.element);
-               var title = self.data('dygraph-title') || state.chart.title;
-               var titleHeight = self.data('dygraph-titleheight') || 19;
-               var hideOverlayOnMouseOut = self.data('dygraph-hideoverlayonmouseout') || true;
-               var colors = self.data('dygraph-colors') || NETDATA.colors;
-
-               // legend
-               var legend = self.data('dygraph-legend') || 'onmouseover';
-               var labelsDiv = self.data('dygraph-labelsdiv') || undefined;
-               var labelsDivStyles = self.data('dygraph-labelsdivstyles') || { 'fontSize':'10px' };
-               var labelsDivWidth = self.data('dygraph-labelsdivwidth') || state.chartWidth() - 70;
-               var labelsSeparateLines = self.data('dygraph-labelsseparatelines') || false;
-               var labelsShowZeroValues = self.data('dygraph-labelsshowzerovalues') || true;
-               var showLabelsOnHighlight = self.data('dygraph-showlabelsonhighlight') || true;
-               var yLabelWidth = self.data('dygraph-ylabelwidth') || 12;
-
-               // Value Formatting
-               var sigFigs = self.data('dygraph-sigfigs') || null;
-               var maxNumberWidth = self.data('dygraph-maxnumberwidth') || 8;
-               var digitsAfterDecimal = self.data('dygraph-digitsafterdecimal') || 2;
-               var valueFormatter = self.data('dygraph-valueformatter') || undefined; //function(x){ return x.toFixed(2); };
-
-               // Axis and Grid
-               var drawAxis = self.data('dygraph-drawaxis') || true;
-               var axisLabelFontSize = self.data('dygraph-axislabelfontsize') || 10;
-               var axisLineWidth = self.data('dygraph-axislinewidth') || 0.3;
-               var gridLineColor = self.data('dygraph-gridlinecolor') || '#EEE';
-               var axisLineColor = self.data('dygraph-axislinecolor') || '#DDD';
-               var drawGrid = self.data('dygraph-drawgrid') || true;
-               var drawXGrid = self.data('dygraph-drawxgrid') || undefined;
-               var drawYGrid = self.data('dygraph-drawygrid') || undefined;
-               var gridLinePattern = self.data('dygraph-gridlinepattern') || null;
-               var gridLineWidth = self.data('dygraph-gridlinewidth') || 0.3;
-
-               // enabling this makes the chart with little square lines
-               var stepPlot = self.data('dygraph-stepplot') || false;
-               var connectSeparatedPoints = self.data('dygraph-connectseparatedpoints') || false;
-
-               // Draw points at the edges of gaps in the data. This improves visibility of small data segments or other data irregularities.
-               var drawGapEdgePoints = self.data('dygraph-drawgapedgepoints') || true;
-
-               // The size of the dot to draw on each point in pixels (see drawPoints). A dot is always drawn when a point is "isolated",
-               // i.e. there is a missing point on either side of it. This also controls the size of those dots.
-               var drawPoints = self.data('dygraph-drawpoints') || false;
-               var pointSize = self.data('dygraph-pointsize') || 1;
-
-               // The width of the lines connecting data points. This can be used to increase the contrast or some graphs.
-               var strokeWidth = self.data('dygraph-strokewidth') || (state.chart.chart_type == 'stacked')?0.0:1.0;
-               var strokePattern = self.data('dygraph-strokepattern') || undefined;
-
-               // Draw a border around graph lines to make crossing lines more easily distinguishable. Useful for graphs with many lines.
-               var strokeBorderColor = self.data('dygraph-strokebordercolor') || 'white';
-               var strokeBorderWidth = self.data('dygraph-strokeborderwidth') || (state.chart.chart_type == 'stacked')?0.0:0.0;
-
-               var highlightCircleSize = self.data('dygraph-highlightcirclesize') || 3;
-               var highlightSeriesOpts = self.data('dygraph-highlightseriesopts') || null; // TOO SLOW: { strokeWidth: 1.5 };
-               var highlightSeriesBackgroundAlpha = self.data('dygraph-highlightseriesbackgroundalpha') || null; // TOO SLOW: (state.chart.chart_type == 'stacked')?0.7:0.5;
-               var pointClickCallback = self.data('dygraph-pointclickcallback') || undefined;
-               var showRangeSelector = self.data('dygraph-showrangeselector') || false;
-               var showRoller = self.data('dygraph-showroller') || false;
-
-               // leave a few pixels empty on the right of the chart
-               var rightGap = self.data('dygraph-rightgap') || 5;
-
-               var fillGraph = self.data('dygraph-fillgraph') || (state.chart.chart_type == 'area')?true:false;
-               var fillAlpha = self.data('dygraph-fillalpha') || (state.chart.chart_type == 'stacked')?0.8:0.2;
-               var stackedGraph = self.data('dygraph-stackedgraph') || (state.chart.chart_type == 'stacked')?true:false;
-               var stackedGraphNaNFill = self.data('dygraph-stackedgraphnanfill') || 'none';
 
                state.dygraph_options = {
-                       title: title,
-                       titleHeight: titleHeight,
-                       ylabel: state.chart.units,
-                       yLabelWidth: yLabelWidth,
-                       connectSeparatedPoints: connectSeparatedPoints,
-                       drawPoints: drawPoints,
-                       fillGraph: fillGraph,
-                       stackedGraph: stackedGraph,
-                       stackedGraphNaNFill: stackedGraphNaNFill,
-                       drawGrid: drawGrid,
-                       drawXGrid: drawXGrid,
-                       drawYGrid: drawYGrid,
-                       gridLinePattern: gridLinePattern,
-                       gridLineWidth: gridLineWidth,
-                       gridLineColor: gridLineColor,
-                       axisLineColor: axisLineColor,
-                       axisLineWidth: axisLineWidth,
-                       drawAxis: drawAxis,
-                       hideOverlayOnMouseOut: hideOverlayOnMouseOut,
-                       labelsDiv: labelsDiv,
-                       labelsDivStyles: labelsDivStyles,
-                       labelsDivWidth: labelsDivWidth,
-                       labelsSeparateLines: labelsSeparateLines,
-                       labelsShowZeroValues: labelsShowZeroValues,
+                       colors: self.data('dygraph-colors') || NETDATA.colors,
+                       
+                       // leave a few pixels empty on the right of the chart
+                       rightGap: self.data('dygraph-rightgap') || 5,
+                       showRangeSelector: self.data('dygraph-showrangeselector') || false,
+                       showRoller: self.data('dygraph-showroller') || false,
+
+                       title: self.data('dygraph-title') || state.chart.title,
+                       titleHeight: self.data('dygraph-titleheight') || 19,
+
+                       legend: self.data('dygraph-legend') || 'onmouseover',
+                       labels: data.result.labels,
+                       labelsDiv: self.data('dygraph-labelsdiv') || undefined,
+                       labelsDivStyles: self.data('dygraph-labelsdivstyles') || { 'fontSize':'10px' },
+                       labelsDivWidth: self.data('dygraph-labelsdivwidth') || state.chartWidth() - 70,
+                       labelsSeparateLines: self.data('dygraph-labelsseparatelines') || false,
+                       labelsShowZeroValues: self.data('dygraph-labelsshowzerovalues') || true,
                        labelsKMB: false,
                        labelsKMG2: false,
-                       legend: legend,
-                       showLabelsOnHighlight: showLabelsOnHighlight,
-                       maxNumberWidth: maxNumberWidth,
-                       sigFigs: sigFigs,
-                       digitsAfterDecimal: digitsAfterDecimal,
-                       axisLabelFontSize: axisLabelFontSize,
-                       colors: colors,
-                       fillAlpha: fillAlpha,
-                       strokeWidth: strokeWidth,
-                       drawGapEdgePoints: drawGapEdgePoints,
-                       pointSize: pointSize,
-                       stepPlot: stepPlot,
-                       strokeBorderColor: strokeBorderColor,
-                       strokeBorderWidth: strokeBorderWidth,
-                       strokePattern: strokePattern,
-                       highlightCircleSize: highlightCircleSize,
-                       highlightSeriesOpts: highlightSeriesOpts,
-                       highlightSeriesBackgroundAlpha: highlightSeriesBackgroundAlpha,
-                       pointClickCallback: pointClickCallback,
-                       showRangeSelector: showRangeSelector,
-                       showRoller: showRoller,
-                       valueFormatter: valueFormatter,
-                       rightGap: rightGap,
-                       labels: data.result.labels,
+                       showLabelsOnHighlight: self.data('dygraph-showlabelsonhighlight') || true,
+                       hideOverlayOnMouseOut: self.data('dygraph-hideoverlayonmouseout') || true,
+
+                       ylabel: state.chart.units,
+                       yLabelWidth: self.data('dygraph-ylabelwidth') || 12,
+
+                       // the function to plot the chart
+                       plotter: null,
+
+                       // The width of the lines connecting data points. This can be used to increase the contrast or some graphs.
+                       strokeWidth: self.data('dygraph-strokewidth') || (state.chart.chart_type == 'stacked')?0.0:1.0,
+                       strokePattern: self.data('dygraph-strokepattern') || undefined,
+
+                       // The size of the dot to draw on each point in pixels (see drawPoints). A dot is always drawn when a point is "isolated",
+                       // i.e. there is a missing point on either side of it. This also controls the size of those dots.
+                       drawPoints: self.data('dygraph-drawpoints') || false,
+                       
+                       // Draw points at the edges of gaps in the data. This improves visibility of small data segments or other data irregularities.
+                       drawGapEdgePoints: self.data('dygraph-drawgapedgepoints') || true,
+
+                       connectSeparatedPoints: self.data('dygraph-connectseparatedpoints') || false,
+                       pointSize: self.data('dygraph-pointsize') || 1,
+
+                       // enabling this makes the chart with little square lines
+                       stepPlot: self.data('dygraph-stepplot') || false,
+                       
+                       // Draw a border around graph lines to make crossing lines more easily distinguishable. Useful for graphs with many lines.
+                       strokeBorderColor: self.data('dygraph-strokebordercolor') || 'white',
+                       strokeBorderWidth: self.data('dygraph-strokeborderwidth') || (state.chart.chart_type == 'stacked')?0.0:0.0,
+
+                       fillGraph: self.data('dygraph-fillgraph') || (state.chart.chart_type == 'area')?true:false,
+                       fillAlpha: self.data('dygraph-fillalpha') || (state.chart.chart_type == 'stacked')?0.8:0.2,
+                       stackedGraph: self.data('dygraph-stackedgraph') || (state.chart.chart_type == 'stacked')?true:false,
+                       stackedGraphNaNFill: self.data('dygraph-stackedgraphnanfill') || 'none',
+                       
+                       drawAxis: self.data('dygraph-drawaxis') || true,
+                       axisLabelFontSize: self.data('dygraph-axislabelfontsize') || 10,
+                       axisLineColor: self.data('dygraph-axislinecolor') || '#DDD',
+                       axisLineWidth: self.data('dygraph-axislinewidth') || 0.3,
+
+                       drawGrid: self.data('dygraph-drawgrid') || true,
+                       drawXGrid: self.data('dygraph-drawxgrid') || undefined,
+                       drawYGrid: self.data('dygraph-drawygrid') || undefined,
+                       gridLinePattern: self.data('dygraph-gridlinepattern') || null,
+                       gridLineWidth: self.data('dygraph-gridlinewidth') || 0.3,
+                       gridLineColor: self.data('dygraph-gridlinecolor') || '#EEE',
+
+                       maxNumberWidth: self.data('dygraph-maxnumberwidth') || 8,
+                       sigFigs: self.data('dygraph-sigfigs') || null,
+                       digitsAfterDecimal: self.data('dygraph-digitsafterdecimal') || 2,
+                       valueFormatter: self.data('dygraph-valueformatter') || undefined, //function(x){ return x.toFixed(2); };,
+
+                       highlightCircleSize: self.data('dygraph-highlightcirclesize') || 4,
+                       highlightSeriesOpts: self.data('dygraph-highlightseriesopts') || null, // TOO SLOW: { strokeWidth: 1.5 },
+                       highlightSeriesBackgroundAlpha: self.data('dygraph-highlightseriesbackgroundalpha') || null, // TOO SLOW: (state.chart.chart_type == 'stacked')?0.7:0.5,
+
+                       pointClickCallback: self.data('dygraph-pointclickcallback') || undefined,
                        axes: {
                                x: {
                                        pixelsPerLabel: 50,
                                if(NETDATA.options.debug.dygraph || state.debug) state.log('dygraphHighlightCallback()');
                                state.pauseChart();
                                NETDATA.dygraph.syncStart(state, event, x, points, row, seriesName);
+                               console.log(state.dygraph_instance);
+                               // fix legend zIndex using the internal structures of dygraph legend module
+                               // this works, but it is a hack!
+                               // state.dygraph_instance.plugins_[0].plugin.legend_div_.style.zIndex = 10000;
                        },
                        unhighlightCallback: function(event) {
                                if(NETDATA.options.debug.dygraph || state.debug) state.log('dygraphUnhighlightCallback()');
                        }
                };
 
+               // smooth lines patch
+               if(NETDATA.dygraph.smooth && state.chart.chart_type == 'line') {
+                       state.dygraph_options.plotter = smoothPlotter;
+                       state.dygraph_options.strokeWidth = 1.5;
+               }
+
+               var theme = self.data('dygraph-theme') || null;
+               if(theme == 'sparkline') {
+                       state.dygraph_options.drawGrid = false;
+                       state.dygraph_options.drawAxis = false;
+                       state.dygraph_options.title = undefined;
+                       state.dygraph_options.units = undefined;
+                       state.dygraph_options.legend = 'never'; // 'follow'
+                       state.dygraph_options.ylabel = undefined;
+                       state.dygraph_options.yLabelWidth = 0;
+                       // state.dygraph_options.labelsDivWidth = 120;
+                       state.dygraph_options.labelsSeparateLines = true;
+                       state.dygraph_options.highlightCircleSize = 3;
+                       state.dygraph_options.rightGap = 0;
+               }
+
                state.dygraph_instance = new Dygraph(state.element_chart,
                        data.result.data, state.dygraph_options);
        };
diff --git a/web/lib/dygraph-smooth-plotter.js b/web/lib/dygraph-smooth-plotter.js
new file mode 100644 (file)
index 0000000..119bf3f
--- /dev/null
@@ -0,0 +1,126 @@
+var smoothPlotter = (function() {
+"use strict";
+
+/**
+ * Given three sequential points, p0, p1 and p2, find the left and right
+ * control points for p1.
+ *
+ * The three points are expected to have x and y properties.
+ *
+ * The alpha parameter controls the amount of smoothing.
+ * If α=0, then both control points will be the same as p1 (i.e. no smoothing).
+ *
+ * Returns [l1x, l1y, r1x, r1y]
+ *
+ * It's guaranteed that the line from (l1x, l1y)-(r1x, r1y) passes through p1.
+ * Unless allowFalseExtrema is set, then it's also guaranteed that:
+ *   l1y ∈ [p0.y, p1.y]
+ *   r1y ∈ [p1.y, p2.y]
+ *
+ * The basic algorithm is:
+ * 1. Put the control points l1 and r1 α of the way down (p0, p1) and (p1, p2).
+ * 2. Shift l1 and r2 so that the line l1–r1 passes through p1
+ * 3. Adjust to prevent false extrema while keeping p1 on the l1–r1 line.
+ *
+ * This is loosely based on the HighCharts algorithm.
+ */
+function getControlPoints(p0, p1, p2, opt_alpha, opt_allowFalseExtrema) {
+  var alpha = (opt_alpha !== undefined) ? opt_alpha : 1/3;  // 0=no smoothing, 1=crazy smoothing
+  var allowFalseExtrema = opt_allowFalseExtrema || false;
+
+  if (!p2) {
+    return [p1.x, p1.y, null, null];
+  }
+
+  // Step 1: Position the control points along each line segment.
+  var l1x = (1 - alpha) * p1.x + alpha * p0.x,
+      l1y = (1 - alpha) * p1.y + alpha * p0.y,
+      r1x = (1 - alpha) * p1.x + alpha * p2.x,
+      r1y = (1 - alpha) * p1.y + alpha * p2.y;
+
+  // Step 2: shift the points up so that p1 is on the l1–r1 line.
+  if (l1x != r1x) {
+    // This can be derived w/ some basic algebra.
+    var deltaY = p1.y - r1y - (p1.x - r1x) * (l1y - r1y) / (l1x - r1x);
+    l1y += deltaY;
+    r1y += deltaY;
+  }
+
+  // Step 3: correct to avoid false extrema.
+  if (!allowFalseExtrema) {
+    if (l1y > p0.y && l1y > p1.y) {
+      l1y = Math.max(p0.y, p1.y);
+      r1y = 2 * p1.y - l1y;
+    } else if (l1y < p0.y && l1y < p1.y) {
+      l1y = Math.min(p0.y, p1.y);
+      r1y = 2 * p1.y - l1y;
+    }
+
+    if (r1y > p1.y && r1y > p2.y) {
+      r1y = Math.max(p1.y, p2.y);
+      l1y = 2 * p1.y - r1y;
+    } else if (r1y < p1.y && r1y < p2.y) {
+      r1y = Math.min(p1.y, p2.y);
+      l1y = 2 * p1.y - r1y;
+    }
+  }
+
+  return [l1x, l1y, r1x, r1y];
+}
+
+
+// A plotter which uses splines to create a smooth curve.
+// See tests/plotters.html for a demo.
+// Can be controlled via smoothPlotter.smoothing
+function smoothPlotter(e) {
+  var ctx = e.drawingContext,
+      points = e.points;
+
+  ctx.beginPath();
+  ctx.moveTo(points[0].canvasx, points[0].canvasy);
+
+  // right control point for previous point
+  var lastRightX = points[0].canvasx, lastRightY = points[0].canvasy;
+  var isOK = Dygraph.isOK;  // i.e. is none of (null, undefined, NaN)
+
+  for (var i = 1; i < points.length; i++) {
+    var p0 = points[i - 1],
+        p1 = points[i],
+        p2 = points[i + 1];
+    p0 = p0 && isOK(p0.canvasy) ? p0 : null;
+    p1 = p1 && isOK(p1.canvasy) ? p1 : null;
+    p2 = p2 && isOK(p2.canvasy) ? p2 : null;
+    if (p0 && p1) {
+      var controls = getControlPoints({x: p0.canvasx, y: p0.canvasy},
+                                      {x: p1.canvasx, y: p1.canvasy},
+                                      p2 && {x: p2.canvasx, y: p2.canvasy},
+                                      smoothPlotter.smoothing);
+      // Uncomment to show the control points:
+      // ctx.lineTo(lastRightX, lastRightY);
+      // ctx.lineTo(controls[0], controls[1]);
+      // ctx.lineTo(p1.canvasx, p1.canvasy);
+      lastRightX = (lastRightX !== null) ? lastRightX : p0.canvasx;
+      lastRightY = (lastRightY !== null) ? lastRightY : p0.canvasy;
+      ctx.bezierCurveTo(lastRightX, lastRightY,
+                        controls[0], controls[1],
+                        p1.canvasx, p1.canvasy);
+      lastRightX = controls[2];
+      lastRightY = controls[3];
+    } else if (p1) {
+      // We're starting again after a missing point.
+      ctx.moveTo(p1.canvasx, p1.canvasy);
+      lastRightX = p1.canvasx;
+      lastRightY = p1.canvasy;
+    } else {
+      lastRightX = lastRightY = null;
+    }
+  }
+
+  ctx.stroke();
+}
+smoothPlotter.smoothing = 1/3;
+smoothPlotter._getControlPoints = getControlPoints;  // for testing
+
+return smoothPlotter;
+
+})();
diff --git a/web/lib/dygraph-synchronizer.js b/web/lib/dygraph-synchronizer.js
deleted file mode 100644 (file)
index bf5b6f3..0000000
+++ /dev/null
@@ -1,229 +0,0 @@
-/**
- * Synchronize zooming and/or selections between a set of dygraphs.
- *
- * Usage:
- *
- *   var g1 = new Dygraph(...),
- *       g2 = new Dygraph(...),
- *       ...;
- *   var sync = Dygraph.synchronize(g1, g2, ...);
- *   // charts are now synchronized
- *   sync.detach();
- *   // charts are no longer synchronized
- *
- * You can set options using the last parameter, for example:
- *
- *   var sync = Dygraph.synchronize(g1, g2, g3, {
- *      selection: true,
- *      zoom: true
- *   });
- *
- * The default is to synchronize both of these.
- *
- * Instead of passing one Dygraph object as each parameter, you may also pass an
- * array of dygraphs:
- *
- *   var sync = Dygraph.synchronize([g1, g2, g3], {
- *      selection: false,
- *      zoom: true
- *   });
- *
- * You may also set `range: false` if you wish to only sync the x-axis.
- * The `range` option has no effect unless `zoom` is true (the default).
- */
-(function() {
-/* global Dygraph:false */
-'use strict';
-
-var Dygraph;
-if (window.Dygraph) {
-  Dygraph = window.Dygraph;
-} else if (typeof(module) !== 'undefined') {
-  Dygraph = require('../dygraph');
-}
-
-var synchronize = function(/* dygraphs..., opts */) {
-  if (arguments.length === 0) {
-    throw 'Invalid invocation of Dygraph.synchronize(). Need >= 1 argument.';
-  }
-
-  var OPTIONS = ['selection', 'zoom', 'range'];
-  var opts = {
-    selection: true,
-    zoom: true,
-    range: true
-  };
-  var dygraphs = [];
-  var prevCallbacks = [];
-
-  var parseOpts = function(obj) {
-    if (!(obj instanceof Object)) {
-      throw 'Last argument must be either Dygraph or Object.';
-    } else {
-      for (var i = 0; i < OPTIONS.length; i++) {
-        var optName = OPTIONS[i];
-        if (obj.hasOwnProperty(optName)) opts[optName] = obj[optName];
-      }
-    }
-  };
-
-  if (arguments[0] instanceof Dygraph) {
-    // Arguments are Dygraph objects.
-    for (var i = 0; i < arguments.length; i++) {
-      if (arguments[i] instanceof Dygraph) {
-        dygraphs.push(arguments[i]);
-      } else {
-        break;
-      }
-    }
-    if (i < arguments.length - 1) {
-      throw 'Invalid invocation of Dygraph.synchronize(). ' +
-            'All but the last argument must be Dygraph objects.';
-    } else if (i == arguments.length - 1) {
-      parseOpts(arguments[arguments.length - 1]);
-    }
-  } else if (arguments[0].length) {
-    // Invoked w/ list of dygraphs, options
-    for (var i = 0; i < arguments[0].length; i++) {
-      dygraphs.push(arguments[0][i]);
-    }
-    if (arguments.length == 2) {
-      parseOpts(arguments[1]);
-    } else if (arguments.length > 2) {
-      throw 'Invalid invocation of Dygraph.synchronize(). ' +
-            'Expected two arguments: array and optional options argument.';
-    }  // otherwise arguments.length == 1, which is fine.
-  } else {
-    throw 'Invalid invocation of Dygraph.synchronize(). ' +
-          'First parameter must be either Dygraph or list of Dygraphs.';
-  }
-
-  if (dygraphs.length < 2) {
-    throw 'Invalid invocation of Dygraph.synchronize(). ' +
-          'Need two or more dygraphs to synchronize.';
-  }
-
-  var readycount = dygraphs.length;
-  for (var i = 0; i < dygraphs.length; i++) {
-    var g = dygraphs[i];
-    g.ready( function() {
-      if (--readycount == 0) {
-        // store original callbacks
-        var callBackTypes = ['drawCallback', 'highlightCallback', 'unhighlightCallback'];
-        for (var j = 0; j < dygraphs.length; j++) {
-          if (!prevCallbacks[j]) {
-            prevCallbacks[j] = {};
-          }
-          for (var k = callBackTypes.length - 1; k >= 0; k--) {
-            prevCallbacks[j][callBackTypes[k]] = dygraphs[j].getFunctionOption(callBackTypes[k]);
-          }
-        }
-
-        // Listen for draw, highlight, unhighlight callbacks.
-        if (opts.zoom) {
-          attachZoomHandlers(dygraphs, opts, prevCallbacks);
-        }
-
-        if (opts.selection) {
-          attachSelectionHandlers(dygraphs, prevCallbacks);
-        }
-      }
-    });
-  }
-
-  return {
-    detach: function() {
-      for (var i = 0; i < dygraphs.length; i++) {
-        var g = dygraphs[i];
-        if (opts.zoom) {
-          g.updateOptions({drawCallback: prevCallbacks[i].drawCallback});
-        }
-        if (opts.selection) {
-          g.updateOptions({
-            highlightCallback: prevCallbacks[i].highlightCallback,
-            unhighlightCallback: prevCallbacks[i].unhighlightCallback
-          });
-        }
-      }
-      // release references & make subsequent calls throw.
-      dygraphs = null;
-      opts = null;
-      prevCallbacks = null;
-    }
-  };
-};
-
-function attachZoomHandlers(gs, syncOpts, prevCallbacks) {
-  var block = false;
-  for (var i = 0; i < gs.length; i++) {
-    var g = gs[i];
-    g.updateOptions({
-      drawCallback: function(me, initial) {
-        if (block || initial) return;
-        block = true;
-        var opts = {
-          dateWindow: me.xAxisRange()
-        };
-        if (syncOpts.range) opts.valueRange = me.yAxisRange();
-
-        for (var j = 0; j < gs.length; j++) {
-          if (gs[j] == me) {
-            if (prevCallbacks[j] && prevCallbacks[j].drawCallback) {
-              prevCallbacks[j].drawCallback.apply(this, arguments);
-            }
-            continue;
-          }
-          gs[j].updateOptions(opts);
-        }
-        block = false;
-      }
-    }, true /* no need to redraw */);
-  }
-}
-
-function attachSelectionHandlers(gs, prevCallbacks) {
-  var block = false;
-  for (var i = 0; i < gs.length; i++) {
-    var g = gs[i];
-
-    g.updateOptions({
-      highlightCallback: function(event, x, points, row, seriesName) {
-        if (block) return;
-        block = true;
-        var me = this;
-        for (var i = 0; i < gs.length; i++) {
-          if (me == gs[i]) {
-            if (prevCallbacks[i] && prevCallbacks[i].highlightCallback) {
-              prevCallbacks[i].highlightCallback.apply(this, arguments);
-            }
-            continue;
-          }
-          var idx = gs[i].getRowForX(x);
-          if (idx !== null) {
-            gs[i].setSelection(idx, seriesName);
-          }
-        }
-        block = false;
-      },
-      unhighlightCallback: function(event) {
-        if (block) return;
-        block = true;
-        var me = this;
-        for (var i = 0; i < gs.length; i++) {
-          if (me == gs[i]) {
-            if (prevCallbacks[i] && prevCallbacks[i].unhighlightCallback) {
-              prevCallbacks[i].unhighlightCallback.apply(this, arguments);
-            }
-            continue;
-          }
-          gs[i].clearSelection();
-        }
-        block = false;
-      }
-    }, true /* no need to redraw */);
-  }
-}
-
-Dygraph.synchronize = synchronize;
-
-})();