Răsfoiți Sursa

HUE-3294 [core] Added nv.d3.sunburst and new nv.d3 utils

Enrico Berti 9 ani în urmă
părinte
comite
3177441ee0

+ 42 - 0
desktop/core/src/desktop/static/desktop/js/nv.d3.dom.js

@@ -0,0 +1,42 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/* Facade for queueing DOM write operations
+ * with Fastdom (https://github.com/wilsonpage/fastdom)
+ * if available.
+ * This could easily be extended to support alternate
+ * implementations in the future.
+ */
+nv.dom.write = function (callback) {
+  if (window.fastdom !== undefined) {
+    return fastdom.mutate(callback);
+  }
+  return callback();
+};
+
+/* Facade for queueing DOM read operations
+ * with Fastdom (https://github.com/wilsonpage/fastdom)
+ * if available.
+ * This could easily be extended to support alternate
+ * implementations in the future.
+ */
+nv.dom.read = function (callback) {
+  if (window.fastdom !== undefined) {
+    return fastdom.measure(callback);
+  }
+  return callback();
+};

+ 1 - 0
desktop/core/src/desktop/static/desktop/js/nv.d3.js

@@ -14,6 +14,7 @@ nv.models = nv.models || {}; //stores all the possible models/components
 nv.charts = {}; //stores all the ready to use charts
 nv.graphs = []; //stores all the graphs currently on the page
 nv.logs = {}; //stores some statistics and potential error messages
+nv.dom = {}; //DOM manipulation functions
 
 nv.dispatch = d3.dispatch('render_start', 'render_end');
 

+ 508 - 0
desktop/core/src/desktop/static/desktop/js/nv.d3.sunburst.js

@@ -0,0 +1,508 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// based on http://bl.ocks.org/kerryrodden/477c1bfb081b783f80ad
+nv.models.sunburst = function () {
+  "use strict";
+
+  //============================================================
+  // Public Variables with Default Settings
+  //------------------------------------------------------------
+
+  var margin = {top: 0, right: 0, bottom: 0, left: 0}
+    , width = 600
+    , height = 600
+    , mode = "count"
+    , modes = {
+    count: function (d) {
+      return 1;
+    }, value: function (d) {
+      return d.value || d.size
+    }, size: function (d) {
+      return d.value || d.size
+    }
+  }
+    , id = Math.floor(Math.random() * 10000) //Create semi-unique ID in case user doesn't select one
+    , container = null
+    , color = nv.utils.defaultColor()
+    , showLabels = false
+    , labelFormat = function (d) {
+    if (mode === 'count') {
+      return d.name + ' #' + d.value
+    } else {
+      return d.name + ' ' + (d.value || d.size)
+    }
+  }
+    , labelThreshold = 0.02
+    , sort = function (d1, d2) {
+    return d1.name > d2.name;
+  }
+    , key = function (d, i) {
+    return d.name;
+  }
+    , groupColorByParent = true
+    , duration = 500
+    , dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMousemove', 'elementMouseover', 'elementMouseout', 'renderEnd');
+
+  //============================================================
+  // aux functions and setup
+  //------------------------------------------------------------
+
+  var x = d3.scale.linear().range([0, 2 * Math.PI]);
+  var y = d3.scale.sqrt();
+
+  var partition = d3.layout.partition().sort(sort);
+
+  var node, availableWidth, availableHeight, radius;
+  var prevPositions = {};
+
+  var arc = d3.svg.arc()
+    .startAngle(function (d) {
+      return Math.max(0, Math.min(2 * Math.PI, x(d.x)))
+    })
+    .endAngle(function (d) {
+      return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)))
+    })
+    .innerRadius(function (d) {
+      return Math.max(0, y(d.y))
+    })
+    .outerRadius(function (d) {
+      return Math.max(0, y(d.y + d.dy))
+    });
+
+  function rotationToAvoidUpsideDown(d) {
+    var centerAngle = computeCenterAngle(d);
+    if (centerAngle > 90) {
+      return 180;
+    }
+    else {
+      return 0;
+    }
+  }
+
+  function computeCenterAngle(d) {
+    var startAngle = Math.max(0, Math.min(2 * Math.PI, x(d.x)));
+    var endAngle = Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
+    var centerAngle = (((startAngle + endAngle) / 2) * (180 / Math.PI)) - 90;
+    return centerAngle;
+  }
+
+  function computeNodePercentage(d) {
+    var startAngle = Math.max(0, Math.min(2 * Math.PI, x(d.x)));
+    var endAngle = Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
+    return (endAngle - startAngle) / (2 * Math.PI);
+  }
+
+  function labelThresholdMatched(d) {
+    var startAngle = Math.max(0, Math.min(2 * Math.PI, x(d.x)));
+    var endAngle = Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
+
+    var size = endAngle - startAngle;
+    return size > labelThreshold;
+  }
+
+  // When zooming: interpolate the scales.
+  function arcTweenZoom(e, i) {
+    var xd = d3.interpolate(x.domain(), [node.x, node.x + node.dx]),
+      yd = d3.interpolate(y.domain(), [node.y, 1]),
+      yr = d3.interpolate(y.range(), [node.y ? 20 : 0, radius]);
+
+    if (i === 0) {
+      return function () {
+        return arc(e);
+      }
+    }
+    else {
+      return function (t) {
+        x.domain(xd(t));
+        y.domain(yd(t)).range(yr(t));
+        return arc(e);
+      }
+    }
+    ;
+  }
+
+  function arcTweenUpdate(d) {
+    var ipo = d3.interpolate({x: d.x0, dx: d.dx0, y: d.y0, dy: d.dy0}, d);
+
+    return function (t) {
+      var b = ipo(t);
+
+      d.x0 = b.x;
+      d.dx0 = b.dx;
+      d.y0 = b.y;
+      d.dy0 = b.dy;
+
+      return arc(b);
+    };
+  }
+
+  function updatePrevPosition(node) {
+    var k = key(node);
+    if (!prevPositions[k]) prevPositions[k] = {};
+    var pP = prevPositions[k];
+    pP.dx = node.dx;
+    pP.x = node.x;
+    pP.dy = node.dy;
+    pP.y = node.y;
+  }
+
+  function storeRetrievePrevPositions(nodes) {
+    nodes.forEach(function (n) {
+      var k = key(n);
+      var pP = prevPositions[k];
+      //console.log(k,n,pP);
+      if (pP) {
+        n.dx0 = pP.dx;
+        n.x0 = pP.x;
+        n.dy0 = pP.dy;
+        n.y0 = pP.y;
+      }
+      else {
+        n.dx0 = n.dx;
+        n.x0 = n.x;
+        n.dy0 = n.dy;
+        n.y0 = n.y;
+      }
+      updatePrevPosition(n);
+    });
+  }
+
+  function zoomClick(d) {
+    var labels = container.selectAll('text')
+    var path = container.selectAll('path')
+
+    // fade out all text elements
+    labels.transition().attr("opacity", 0);
+
+    // to allow reference to the new center node
+    node = d;
+
+    path.transition()
+      .duration(duration)
+      .attrTween("d", arcTweenZoom)
+      .each('end', function (e) {
+        // partially taken from here: http://bl.ocks.org/metmajer/5480307
+        // check if the animated element's data e lies within the visible angle span given in d
+        if (e.x >= d.x && e.x < (d.x + d.dx)) {
+          if (e.depth >= d.depth) {
+            // get a selection of the associated text element
+            var parentNode = d3.select(this.parentNode);
+            var arcText = parentNode.select('text');
+
+            // fade in the text element and recalculate positions
+            arcText.transition().duration(duration)
+              .text(function (e) {
+                return labelFormat(e)
+              })
+              .attr("opacity", function (d) {
+                if (labelThresholdMatched(d)) {
+                  return 1;
+                }
+                else {
+                  return 0;
+                }
+              })
+              .attr("transform", function () {
+                var width = this.getBBox().width;
+                if (e.depth === 0)
+                  return "translate(" + (width / 2 * -1) + ",0)";
+                else if (e.depth === d.depth) {
+                  return "translate(" + (y(e.y) + 5) + ",0)";
+                }
+                else {
+                  var centerAngle = computeCenterAngle(e);
+                  var rotation = rotationToAvoidUpsideDown(e);
+                  if (rotation === 0) {
+                    return 'rotate(' + centerAngle + ')translate(' + (y(e.y) + 5) + ',0)';
+                  }
+                  else {
+                    return 'rotate(' + centerAngle + ')translate(' + (y(e.y) + width + 5) + ',0)rotate(' + rotation + ')';
+                  }
+                }
+              });
+          }
+        }
+      })
+  }
+
+  //============================================================
+  // chart function
+  //------------------------------------------------------------
+  var renderWatch = nv.utils.renderWatch(dispatch);
+
+  function chart(selection) {
+    renderWatch.reset();
+
+    selection.each(function (data) {
+      container = d3.select(this);
+      availableWidth = nv.utils.availableWidth(width, container, margin);
+      availableHeight = nv.utils.availableHeight(height, container, margin);
+      radius = Math.min(availableWidth, availableHeight) / 2;
+
+      y.range([0, radius]);
+
+      // Setup containers and skeleton of chart
+      var wrap = container.select('g.nvd3.nv-wrap.nv-sunburst');
+      if (!wrap[0][0]) {
+        wrap = container.append('g')
+          .attr('class', 'nvd3 nv-wrap nv-sunburst nv-chart-' + id)
+          .attr('transform', 'translate(' + ((availableWidth / 2) + margin.left + margin.right) + ',' + ((availableHeight / 2) + margin.top + margin.bottom) + ')');
+      } else {
+        wrap.attr('transform', 'translate(' + ((availableWidth / 2) + margin.left + margin.right) + ',' + ((availableHeight / 2) + margin.top + margin.bottom) + ')');
+      }
+
+      container.on('click', function (d, i) {
+        dispatch.chartClick({
+          data: d,
+          index: i,
+          pos: d3.event,
+          id: id
+        });
+      });
+
+      partition.value(modes[mode] || modes["count"]);
+
+      //reverse the drawing order so that the labels of inner
+      //arcs are drawn on top of the outer arcs.
+      var nodes = partition.nodes(data[0]).reverse()
+
+      storeRetrievePrevPositions(nodes);
+      var cG = wrap.selectAll('.arc-container').data(nodes, key)
+
+      //handle new datapoints
+      var cGE = cG.enter()
+        .append("g")
+        .attr("class", 'arc-container')
+
+      cGE.append("path")
+        .attr("d", arc)
+        .style("fill", function (d) {
+          if (d.color) {
+            return d.color;
+          }
+          else if (groupColorByParent) {
+            return color((d.children ? d : d.parent).name);
+          }
+          else {
+            return color(d.name);
+          }
+        })
+        .style("stroke", "#FFF")
+        .on("click", function (d, i) {
+          zoomClick(d);
+          dispatch.elementClick({
+            data: d,
+            index: i
+          })
+        })
+        .on('mouseover', function (d, i) {
+          d3.select(this).classed('hover', true).style('opacity', 0.8);
+          dispatch.elementMouseover({
+            data: d,
+            color: d3.select(this).style("fill"),
+            percent: computeNodePercentage(d)
+          });
+        })
+        .on('mouseout', function (d, i) {
+          d3.select(this).classed('hover', false).style('opacity', 1);
+          dispatch.elementMouseout({
+            data: d
+          });
+        })
+        .on('mousemove', function (d, i) {
+          dispatch.elementMousemove({
+            data: d
+          });
+        });
+
+      ///Iterating via each and selecting based on the this
+      ///makes it work ... a cG.selectAll('path') doesn't.
+      ///Without iteration the data (in the element) didn't update.
+      cG.each(function (d) {
+        d3.select(this).select('path')
+          .transition()
+          .duration(duration)
+          .attrTween('d', arcTweenUpdate);
+      });
+
+      if (showLabels) {
+        //remove labels first and add them back
+        cG.selectAll('text').remove();
+
+        //this way labels are on top of newly added arcs
+        cG.append('text')
+          .text(function (e) {
+            return labelFormat(e)
+          })
+          .transition()
+          .duration(duration)
+          .attr("opacity", function (d) {
+            if (labelThresholdMatched(d)) {
+              return 1;
+            }
+            else {
+              return 0;
+            }
+          })
+          .attr("transform", function (d) {
+            var width = this.getBBox().width;
+            if (d.depth === 0) {
+              return "rotate(0)translate(" + (width / 2 * -1) + ",0)";
+            }
+            else {
+              var centerAngle = computeCenterAngle(d);
+              var rotation = rotationToAvoidUpsideDown(d);
+              if (rotation === 0) {
+                return 'rotate(' + centerAngle + ')translate(' + (y(d.y) + 5) + ',0)';
+              }
+              else {
+                return 'rotate(' + centerAngle + ')translate(' + (y(d.y) + width + 5) + ',0)rotate(' + rotation + ')';
+              }
+            }
+          });
+      }
+
+      //zoom out to the center when the data is updated.
+      zoomClick(nodes[nodes.length - 1])
+
+
+      //remove unmatched elements ...
+      cG.exit()
+        .transition()
+        .duration(duration)
+        .attr('opacity', 0)
+        .each('end', function (d) {
+          var k = key(d);
+          prevPositions[k] = undefined;
+        })
+        .remove();
+    });
+
+
+    renderWatch.renderEnd('sunburst immediate');
+    return chart;
+  }
+
+  //============================================================
+  // Expose Public Variables
+  //------------------------------------------------------------
+
+  chart.dispatch = dispatch;
+  chart.options = nv.utils.optionsFunc.bind(chart);
+
+  chart._options = Object.create({}, {
+    // simple options, just get/set the necessary values
+    width: {
+      get: function () {
+        return width;
+      }, set: function (_) {
+        width = _;
+      }
+    },
+    height: {
+      get: function () {
+        return height;
+      }, set: function (_) {
+        height = _;
+      }
+    },
+    mode: {
+      get: function () {
+        return mode;
+      }, set: function (_) {
+        mode = _;
+      }
+    },
+    id: {
+      get: function () {
+        return id;
+      }, set: function (_) {
+        id = _;
+      }
+    },
+    duration: {
+      get: function () {
+        return duration;
+      }, set: function (_) {
+        duration = _;
+      }
+    },
+    groupColorByParent: {
+      get: function () {
+        return groupColorByParent;
+      }, set: function (_) {
+        groupColorByParent = !!_;
+      }
+    },
+    showLabels: {
+      get: function () {
+        return showLabels;
+      }, set: function (_) {
+        showLabels = !!_
+      }
+    },
+    labelFormat: {
+      get: function () {
+        return labelFormat;
+      }, set: function (_) {
+        labelFormat = _
+      }
+    },
+    labelThreshold: {
+      get: function () {
+        return labelThreshold;
+      }, set: function (_) {
+        labelThreshold = _
+      }
+    },
+    sort: {
+      get: function () {
+        return sort;
+      }, set: function (_) {
+        sort = _
+      }
+    },
+    key: {
+      get: function () {
+        return key;
+      }, set: function (_) {
+        key = _
+      }
+    },
+    // options that require extra logic in the setter
+    margin: {
+      get: function () {
+        return margin;
+      }, set: function (_) {
+        margin.top = _.top != undefined ? _.top : margin.top;
+        margin.right = _.right != undefined ? _.right : margin.right;
+        margin.bottom = _.bottom != undefined ? _.bottom : margin.bottom;
+        margin.left = _.left != undefined ? _.left : margin.left;
+      }
+    },
+    color: {
+      get: function () {
+        return color;
+      }, set: function (_) {
+        color = nv.utils.getColor(_);
+      }
+    }
+  });
+
+  nv.utils.initOptions(chart);
+  return chart;
+};

+ 188 - 0
desktop/core/src/desktop/static/desktop/js/nv.d3.sunburstChart.js

@@ -0,0 +1,188 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+nv.models.sunburstChart = function () {
+  "use strict";
+
+  //============================================================
+  // Public Variables with Default Settings
+  //------------------------------------------------------------
+
+  var sunburst = nv.models.sunburst();
+  var tooltip = nv.models.tooltip();
+
+  var margin = {top: 30, right: 20, bottom: 20, left: 20}
+    , width = null
+    , height = null
+    , color = nv.utils.defaultColor()
+    , showTooltipPercent = false
+    , id = Math.round(Math.random() * 100000)
+    , defaultState = null
+    , noData = null
+    , duration = 250
+    , dispatch = d3.dispatch('stateChange', 'changeState', 'renderEnd');
+
+
+  //============================================================
+  // Private Variables
+  //------------------------------------------------------------
+
+  var renderWatch = nv.utils.renderWatch(dispatch);
+
+  tooltip
+    .duration(0)
+    .headerEnabled(false)
+    .valueFormatter(function (d) {
+      return d;
+    });
+
+  //============================================================
+  // Chart function
+  //------------------------------------------------------------
+
+  function chart(selection) {
+    renderWatch.reset();
+    renderWatch.models(sunburst);
+
+    selection.each(function (data) {
+      var container = d3.select(this);
+
+      nv.utils.initSVG(container);
+
+      var availableWidth = nv.utils.availableWidth(width, container, margin);
+      var availableHeight = nv.utils.availableHeight(height, container, margin);
+
+      chart.update = function () {
+        if (duration === 0) {
+          container.call(chart);
+        } else {
+          container.transition().duration(duration).call(chart);
+        }
+      };
+      chart.container = container;
+
+      // Display No Data message if there's nothing to show.
+      if (!data || !data.length) {
+        nv.utils.noData(chart, container);
+        return chart;
+      } else {
+        container.selectAll('.nv-noData').remove();
+      }
+
+      sunburst.width(availableWidth).height(availableHeight).margin(margin);
+      container.call(sunburst);
+    });
+
+    renderWatch.renderEnd('sunburstChart immediate');
+    return chart;
+  }
+
+  //============================================================
+  // Event Handling/Dispatching (out of chart's scope)
+  //------------------------------------------------------------
+
+  sunburst.dispatch.on('elementMouseover.tooltip', function (evt) {
+    evt.series = {
+      key: evt.data.name,
+      value: (evt.data.value || evt.data.size),
+      color: evt.color,
+      percent: evt.percent
+    };
+    if (!showTooltipPercent) {
+      delete evt.percent;
+      delete evt.series.percent;
+    }
+    tooltip.data(evt).hidden(false);
+  });
+
+  sunburst.dispatch.on('elementMouseout.tooltip', function (evt) {
+    tooltip.hidden(true);
+  });
+
+  sunburst.dispatch.on('elementMousemove.tooltip', function (evt) {
+    tooltip();
+  });
+
+  //============================================================
+  // Expose Public Variables
+  //------------------------------------------------------------
+
+  // expose chart's sub-components
+  chart.dispatch = dispatch;
+  chart.sunburst = sunburst;
+  chart.tooltip = tooltip;
+  chart.options = nv.utils.optionsFunc.bind(chart);
+
+  // use Object get/set functionality to map between vars and chart functions
+  chart._options = Object.create({}, {
+    // simple options, just get/set the necessary values
+    noData: {
+      get: function () {
+        return noData;
+      }, set: function (_) {
+        noData = _;
+      }
+    },
+    defaultState: {
+      get: function () {
+        return defaultState;
+      }, set: function (_) {
+        defaultState = _;
+      }
+    },
+    showTooltipPercent: {
+      get: function () {
+        return showTooltipPercent;
+      }, set: function (_) {
+        showTooltipPercent = _;
+      }
+    },
+
+    // options that require extra logic in the setter
+    color: {
+      get: function () {
+        return color;
+      }, set: function (_) {
+        color = _;
+        sunburst.color(color);
+      }
+    },
+    duration: {
+      get: function () {
+        return duration;
+      }, set: function (_) {
+        duration = _;
+        renderWatch.reset(duration);
+        sunburst.duration(duration);
+      }
+    },
+    margin: {
+      get: function () {
+        return margin;
+      }, set: function (_) {
+        margin.top = _.top !== undefined ? _.top : margin.top;
+        margin.right = _.right !== undefined ? _.right : margin.right;
+        margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom;
+        margin.left = _.left !== undefined ? _.left : margin.left;
+        sunburst.margin(margin);
+      }
+    }
+  });
+  nv.utils.inheritOptions(chart, sunburst);
+  nv.utils.initOptions(chart);
+  return chart;
+
+};

+ 498 - 0
desktop/core/src/desktop/static/desktop/js/nv.d3.tooltip.js

@@ -0,0 +1,498 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/* Model which can be instantiated to handle tooltip rendering.
+ Example usage:
+ var tip = nv.models.tooltip().gravity('w').distance(23)
+ .data(myDataObject);
+
+ tip();    //just invoke the returned function to render tooltip.
+ */
+nv.models.tooltip = function () {
+  "use strict";
+
+  /*
+   Tooltip data. If data is given in the proper format, a consistent tooltip is generated.
+   Example Format of data:
+   {
+   key: "Date",
+   value: "August 2009",
+   series: [
+   {key: "Series 1", value: "Value 1", color: "#000"},
+   {key: "Series 2", value: "Value 2", color: "#00f"}
+   ]
+   }
+   */
+  var id = "nvtooltip-" + Math.floor(Math.random() * 100000) // Generates a unique id when you create a new tooltip() object.
+    , data = null
+    , gravity = 'w'   // Can be 'n','s','e','w'. Determines how tooltip is positioned.
+    , distance = 25 // Distance to offset tooltip from the mouse location.
+    , snapDistance = 0   // Tolerance allowed before tooltip is moved from its current position (creates 'snapping' effect)
+    , classes = null  // Attaches additional CSS classes to the tooltip DIV that is created.
+    , hidden = true  // Start off hidden, toggle with hide/show functions below.
+    , hideDelay = 200  // Delay (in ms) before the tooltip hides after calling hide().
+    , tooltip = null // d3 select of the tooltip div.
+    , lastPosition = {left: null, top: null} // Last position the tooltip was in.
+    , enabled = true  // True -> tooltips are rendered. False -> don't render tooltips.
+    , duration = 100 // Tooltip movement duration, in ms.
+    , headerEnabled = true // If is to show the tooltip header.
+    , nvPointerEventsClass = "nv-pointer-events-none" // CSS class to specify whether element should not have mouse events.
+    ;
+
+  // Format function for the tooltip values column.
+  var valueFormatter = function (d, i) {
+    return d;
+  };
+
+  // Format function for the tooltip header value.
+  var headerFormatter = function (d) {
+    return d;
+  };
+
+  var keyFormatter = function (d, i) {
+    return d;
+  };
+
+  // By default, the tooltip model renders a beautiful table inside a DIV.
+  // You can override this function if a custom tooltip is desired.
+  var contentGenerator = function (d) {
+    if (d === null) {
+      return '';
+    }
+
+    var table = d3.select(document.createElement("table"));
+    if (headerEnabled) {
+      var theadEnter = table.selectAll("thead")
+        .data([d])
+        .enter().append("thead");
+
+      theadEnter.append("tr")
+        .append("td")
+        .attr("colspan", 3)
+        .append("strong")
+        .classed("x-value", true)
+        .html(headerFormatter(d.value));
+    }
+
+    var tbodyEnter = table.selectAll("tbody")
+      .data([d])
+      .enter().append("tbody");
+
+    var trowEnter = tbodyEnter.selectAll("tr")
+      .data(function (p) {
+        return p.series
+      })
+      .enter()
+      .append("tr")
+      .classed("highlight", function (p) {
+        return p.highlight
+      });
+
+    trowEnter.append("td")
+      .classed("legend-color-guide", true)
+      .append("div")
+      .style("background-color", function (p) {
+        return p.color
+      });
+
+    trowEnter.append("td")
+      .classed("key", true)
+      .classed("total", function (p) {
+        return !!p.total
+      })
+      .html(function (p, i) {
+        return keyFormatter(p.key, i)
+      });
+
+    trowEnter.append("td")
+      .classed("value", true)
+      .html(function (p, i) {
+        return valueFormatter(p.value, i)
+      });
+
+    trowEnter.filter(function (p, i) {
+      return p.percent !== undefined
+    }).append("td")
+      .classed("percent", true)
+      .html(function (p, i) {
+        return "(" + d3.format('%')(p.percent) + ")"
+      });
+
+    trowEnter.selectAll("td").each(function (p) {
+      if (p.highlight) {
+        var opacityScale = d3.scale.linear().domain([0, 1]).range(["#fff", p.color]);
+        var opacity = 0.6;
+        d3.select(this)
+          .style("border-bottom-color", opacityScale(opacity))
+          .style("border-top-color", opacityScale(opacity))
+        ;
+      }
+    });
+
+    var html = table.node().outerHTML;
+    if (d.footer !== undefined)
+      html += "<div class='footer'>" + d.footer + "</div>";
+    return html;
+
+  };
+
+  /*
+   Function that returns the position (relative to the viewport/document.body)
+   the tooltip should be placed in.
+   Should return: {
+   left: <leftPos>,
+   top: <topPos>
+   }
+   */
+  var position = function () {
+    var pos = {
+      left: d3.event !== null ? d3.event.clientX : 0,
+      top: d3.event !== null ? d3.event.clientY : 0
+    };
+
+    if (getComputedStyle(document.body).transform != 'none') {
+      // Take the offset into account, as now the tooltip is relative
+      // to document.body.
+      var client = document.body.getBoundingClientRect();
+      pos.left -= client.left;
+      pos.top -= client.top;
+    }
+
+    return pos;
+  };
+
+  var dataSeriesExists = function (d) {
+    if (d && d.series) {
+      if (nv.utils.isArray(d.series)) {
+        return true;
+      }
+      // if object, it's okay just convert to array of the object
+      if (nv.utils.isObject(d.series)) {
+        d.series = [d.series];
+        return true;
+      }
+    }
+    return false;
+  };
+
+  // Calculates the gravity offset of the tooltip. Parameter is position of tooltip
+  // relative to the viewport.
+  var calcGravityOffset = function (pos) {
+    var height = tooltip.node().offsetHeight,
+      width = tooltip.node().offsetWidth,
+      clientWidth = document.documentElement.clientWidth, // Don't want scrollbars.
+      clientHeight = document.documentElement.clientHeight, // Don't want scrollbars.
+      left, top, tmp;
+
+    // calculate position based on gravity
+    switch (gravity) {
+      case 'e':
+        left = -width - distance;
+        top = -(height / 2);
+        if (pos.left + left < 0) left = distance;
+        if ((tmp = pos.top + top) < 0) top -= tmp;
+        if ((tmp = pos.top + top + height) > clientHeight) top -= tmp - clientHeight;
+        break;
+      case 'w':
+        left = distance;
+        top = -(height / 2);
+        if (pos.left + left + width > clientWidth) left = -width - distance;
+        if ((tmp = pos.top + top) < 0) top -= tmp;
+        if ((tmp = pos.top + top + height) > clientHeight) top -= tmp - clientHeight;
+        break;
+      case 'n':
+        left = -(width / 2) - 5; // - 5 is an approximation of the mouse's height.
+        top = distance;
+        if (pos.top + top + height > clientHeight) top = -height - distance;
+        if ((tmp = pos.left + left) < 0) left -= tmp;
+        if ((tmp = pos.left + left + width) > clientWidth) left -= tmp - clientWidth;
+        break;
+      case 's':
+        left = -(width / 2);
+        top = -height - distance;
+        if (pos.top + top < 0) top = distance;
+        if ((tmp = pos.left + left) < 0) left -= tmp;
+        if ((tmp = pos.left + left + width) > clientWidth) left -= tmp - clientWidth;
+        break;
+      case 'center':
+        left = -(width / 2);
+        top = -(height / 2);
+        break;
+      default:
+        left = 0;
+        top = 0;
+        break;
+    }
+
+    return {'left': left, 'top': top};
+  };
+
+  /*
+   Positions the tooltip in the correct place, as given by the position() function.
+   */
+  var positionTooltip = function () {
+    nv.dom.read(function () {
+      var pos = position(),
+        gravityOffset = calcGravityOffset(pos),
+        left = pos.left + gravityOffset.left,
+        top = pos.top + gravityOffset.top;
+
+      // delay hiding a bit to avoid flickering
+      if (hidden) {
+        tooltip
+          .interrupt()
+          .transition()
+          .delay(hideDelay)
+          .duration(0)
+          .style('opacity', 0);
+      } else {
+        // using tooltip.style('transform') returns values un-usable for tween
+        var old_translate = 'translate(' + lastPosition.left + 'px, ' + lastPosition.top + 'px)';
+        var new_translate = 'translate(' + Math.round(left) + 'px, ' + Math.round(top) + 'px)';
+        var translateInterpolator = d3.interpolateString(old_translate, new_translate);
+        var is_hidden = tooltip.style('opacity') < 0.1;
+
+        tooltip
+          .interrupt() // cancel running transitions
+          .transition()
+          .duration(is_hidden ? 0 : duration)
+          // using tween since some versions of d3 can't auto-tween a translate on a div
+          .styleTween('transform', function (d) {
+            return translateInterpolator;
+          }, 'important')
+          // Safari has its own `-webkit-transform` and does not support `transform`
+          .styleTween('-webkit-transform', function (d) {
+            return translateInterpolator;
+          })
+          .style('-ms-transform', new_translate)
+          .style('opacity', 1);
+      }
+
+      lastPosition.left = left;
+      lastPosition.top = top;
+    });
+  };
+
+  // Creates new tooltip container, or uses existing one on DOM.
+  function initTooltip() {
+    if (!tooltip || !tooltip.node()) {
+      // Create new tooltip div if it doesn't exist on DOM.
+
+      var data = [1];
+      tooltip = d3.select(document.body).select('#' + id).data(data);
+
+      tooltip.enter().append('div')
+        .attr("class", "nvtooltip " + (classes ? classes : "xy-tooltip"))
+        .attr("id", id)
+        .style("top", 0).style("left", 0)
+        .style('opacity', 0)
+        .style('position', 'fixed')
+        .selectAll("div, table, td, tr").classed(nvPointerEventsClass, true)
+        .classed(nvPointerEventsClass, true);
+
+      tooltip.exit().remove()
+    }
+  }
+
+  // Draw the tooltip onto the DOM.
+  function nvtooltip() {
+    if (!enabled) return;
+    if (!dataSeriesExists(data)) return;
+
+    nv.dom.write(function () {
+      initTooltip();
+      // Generate data and set it into tooltip.
+      // Bonus - If you override contentGenerator and return falsey you can use something like
+      //         React or Knockout to bind the data for your tooltip.
+      var newContent = contentGenerator(data);
+      if (newContent) {
+        tooltip.node().innerHTML = newContent;
+      }
+
+      positionTooltip();
+    });
+
+    return nvtooltip;
+  }
+
+  nvtooltip.nvPointerEventsClass = nvPointerEventsClass;
+  nvtooltip.options = nv.utils.optionsFunc.bind(nvtooltip);
+
+  nvtooltip._options = Object.create({}, {
+    // simple read/write options
+    duration: {
+      get: function () {
+        return duration;
+      }, set: function (_) {
+        duration = _;
+      }
+    },
+    gravity: {
+      get: function () {
+        return gravity;
+      }, set: function (_) {
+        gravity = _;
+      }
+    },
+    distance: {
+      get: function () {
+        return distance;
+      }, set: function (_) {
+        distance = _;
+      }
+    },
+    snapDistance: {
+      get: function () {
+        return snapDistance;
+      }, set: function (_) {
+        snapDistance = _;
+      }
+    },
+    classes: {
+      get: function () {
+        return classes;
+      }, set: function (_) {
+        classes = _;
+      }
+    },
+    enabled: {
+      get: function () {
+        return enabled;
+      }, set: function (_) {
+        enabled = _;
+      }
+    },
+    hideDelay: {
+      get: function () {
+        return hideDelay;
+      }, set: function (_) {
+        hideDelay = _;
+      }
+    },
+    contentGenerator: {
+      get: function () {
+        return contentGenerator;
+      }, set: function (_) {
+        contentGenerator = _;
+      }
+    },
+    valueFormatter: {
+      get: function () {
+        return valueFormatter;
+      }, set: function (_) {
+        valueFormatter = _;
+      }
+    },
+    headerFormatter: {
+      get: function () {
+        return headerFormatter;
+      }, set: function (_) {
+        headerFormatter = _;
+      }
+    },
+    keyFormatter: {
+      get: function () {
+        return keyFormatter;
+      }, set: function (_) {
+        keyFormatter = _;
+      }
+    },
+    headerEnabled: {
+      get: function () {
+        return headerEnabled;
+      }, set: function (_) {
+        headerEnabled = _;
+      }
+    },
+    position: {
+      get: function () {
+        return position;
+      }, set: function (_) {
+        position = _;
+      }
+    },
+
+    // Deprecated options
+    chartContainer: {
+      get: function () {
+        return document.body;
+      }, set: function (_) {
+        // deprecated after 1.8.3
+        nv.deprecated('chartContainer', 'feature removed after 1.8.3');
+      }
+    },
+    fixedTop: {
+      get: function () {
+        return null;
+      }, set: function (_) {
+        // deprecated after 1.8.1
+        nv.deprecated('fixedTop', 'feature removed after 1.8.1');
+      }
+    },
+    offset: {
+      get: function () {
+        return {left: 0, top: 0};
+      }, set: function (_) {
+        // deprecated after 1.8.1
+        nv.deprecated('offset', 'use chart.tooltip.distance() instead');
+      }
+    },
+
+    // options with extra logic
+    hidden: {
+      get: function () {
+        return hidden;
+      }, set: function (_) {
+        if (hidden != _) {
+          hidden = !!_;
+          nvtooltip();
+        }
+      }
+    },
+    data: {
+      get: function () {
+        return data;
+      }, set: function (_) {
+        // if showing a single data point, adjust data format with that
+        if (_.point) {
+          _.value = _.point.x;
+          _.series = _.series || {};
+          _.series.value = _.point.y;
+          _.series.color = _.point.color || _.series.color;
+        }
+        data = _;
+      }
+    },
+
+    // read only properties
+    node: {
+      get: function () {
+        return tooltip.node();
+      }, set: function (_) {
+      }
+    },
+    id: {
+      get: function () {
+        return id;
+      }, set: function (_) {
+      }
+    }
+  });
+
+  nv.utils.initOptions(nvtooltip);
+  return nvtooltip;
+};

+ 747 - 0
desktop/core/src/desktop/static/desktop/js/nv.d3.utils.js

@@ -0,0 +1,747 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/*
+ Gets the browser window size
+
+ Returns object with height and width properties
+ */
+nv.utils.windowSize = function () {
+  // Sane defaults
+  var size = {width: 640, height: 480};
+
+  // Most recent browsers use
+  if (window.innerWidth && window.innerHeight) {
+    size.width = window.innerWidth;
+    size.height = window.innerHeight;
+    return (size);
+  }
+
+  // IE can use depending on mode it is in
+  if (document.compatMode == 'CSS1Compat' &&
+    document.documentElement &&
+    document.documentElement.offsetWidth) {
+
+    size.width = document.documentElement.offsetWidth;
+    size.height = document.documentElement.offsetHeight;
+    return (size);
+  }
+
+  // Earlier IE uses Doc.body
+  if (document.body && document.body.offsetWidth) {
+    size.width = document.body.offsetWidth;
+    size.height = document.body.offsetHeight;
+    return (size);
+  }
+
+  return (size);
+};
+
+
+/* handle dumb browser quirks...  isinstance breaks if you use frames
+ typeof returns 'object' for null, NaN is a number, etc.
+ */
+nv.utils.isArray = Array.isArray;
+nv.utils.isObject = function (a) {
+  return a !== null && typeof a === 'object';
+};
+nv.utils.isFunction = function (a) {
+  return typeof a === 'function';
+};
+nv.utils.isDate = function (a) {
+  return toString.call(a) === '[object Date]';
+};
+nv.utils.isNumber = function (a) {
+  return !isNaN(a) && typeof a === 'number';
+};
+
+
+/*
+ Binds callback function to run when window is resized
+ */
+nv.utils.windowResize = function (handler) {
+  if (window.addEventListener) {
+    window.addEventListener('resize', handler);
+  } else {
+    nv.log("ERROR: Failed to bind to window.resize with: ", handler);
+  }
+  // return object with clear function to remove the single added callback.
+  return {
+    callback: handler,
+    clear: function () {
+      window.removeEventListener('resize', handler);
+    }
+  }
+};
+
+
+/*
+ Backwards compatible way to implement more d3-like coloring of graphs.
+ Can take in nothing, an array, or a function/scale
+ To use a normal scale, get the range and pass that because we must be able
+ to take two arguments and use the index to keep backward compatibility
+ */
+nv.utils.getColor = function (color) {
+  //if you pass in nothing, get default colors back
+  if (color === undefined) {
+    return nv.utils.defaultColor();
+
+    //if passed an array, turn it into a color scale
+  } else if (nv.utils.isArray(color)) {
+    var color_scale = d3.scale.ordinal().range(color);
+    return function (d, i) {
+      var key = i === undefined ? d : i;
+      return d.color || color_scale(key);
+    };
+
+    //if passed a function or scale, return it, or whatever it may be
+    //external libs, such as angularjs-nvd3-directives use this
+  } else {
+    //can't really help it if someone passes rubbish as color
+    return color;
+  }
+};
+
+
+/*
+ Default color chooser uses a color scale of 20 colors from D3
+ https://github.com/mbostock/d3/wiki/Ordinal-Scales#categorical-colors
+ */
+nv.utils.defaultColor = function () {
+  // get range of the scale so we'll turn it into our own function.
+  return nv.utils.getColor(d3.scale.category20().range());
+};
+
+
+/*
+ Returns a color function that takes the result of 'getKey' for each series and
+ looks for a corresponding color from the dictionary
+ */
+nv.utils.customTheme = function (dictionary, getKey, defaultColors) {
+  // use default series.key if getKey is undefined
+  getKey = getKey || function (series) {
+      return series.key
+    };
+  defaultColors = defaultColors || d3.scale.category20().range();
+
+  // start at end of default color list and walk back to index 0
+  var defIndex = defaultColors.length;
+
+  return function (series, index) {
+    var key = getKey(series);
+    if (nv.utils.isFunction(dictionary[key])) {
+      return dictionary[key]();
+    } else if (dictionary[key] !== undefined) {
+      return dictionary[key];
+    } else {
+      // no match in dictionary, use a default color
+      if (!defIndex) {
+        // used all the default colors, start over
+        defIndex = defaultColors.length;
+      }
+      defIndex = defIndex - 1;
+      return defaultColors[defIndex];
+    }
+  };
+};
+
+
+/*
+ From the PJAX example on d3js.org, while this is not really directly needed
+ it's a very cool method for doing pjax, I may expand upon it a little bit,
+ open to suggestions on anything that may be useful
+ */
+nv.utils.pjax = function (links, content) {
+
+  var load = function (href) {
+    d3.html(href, function (fragment) {
+      var target = d3.select(content).node();
+      target.parentNode.replaceChild(
+        d3.select(fragment).select(content).node(),
+        target);
+      nv.utils.pjax(links, content);
+    });
+  };
+
+  d3.selectAll(links).on("click", function () {
+    history.pushState(this.href, this.textContent, this.href);
+    load(this.href);
+    d3.event.preventDefault();
+  });
+
+  d3.select(window).on("popstate", function () {
+    if (d3.event.state) {
+      load(d3.event.state);
+    }
+  });
+};
+
+
+/*
+ For when we want to approximate the width in pixels for an SVG:text element.
+ Most common instance is when the element is in a display:none; container.
+ Forumla is : text.length * font-size * constant_factor
+ */
+nv.utils.calcApproxTextWidth = function (svgTextElem) {
+  if (nv.utils.isFunction(svgTextElem.style) && nv.utils.isFunction(svgTextElem.text)) {
+    var fontSize = parseInt(svgTextElem.style("font-size").replace("px", ""), 10);
+    var textLength = svgTextElem.text().length;
+    return nv.utils.NaNtoZero(textLength * fontSize * 0.5);
+  }
+  return 0;
+};
+
+
+/*
+ Numbers that are undefined, null or NaN, convert them to zeros.
+ */
+nv.utils.NaNtoZero = function (n) {
+  if (!nv.utils.isNumber(n)
+    || isNaN(n)
+    || n === null
+    || n === Infinity
+    || n === -Infinity) {
+
+    return 0;
+  }
+  return n;
+};
+
+/*
+ Add a way to watch for d3 transition ends to d3
+ */
+d3.selection.prototype.watchTransition = function (renderWatch) {
+  var args = [this].concat([].slice.call(arguments, 1));
+  return renderWatch.transition.apply(renderWatch, args);
+};
+
+
+/*
+ Helper object to watch when d3 has rendered something
+ */
+nv.utils.renderWatch = function (dispatch, duration) {
+  if (!(this instanceof nv.utils.renderWatch)) {
+    return new nv.utils.renderWatch(dispatch, duration);
+  }
+
+  var _duration = duration !== undefined ? duration : 250;
+  var renderStack = [];
+  var self = this;
+
+  this.models = function (models) {
+    models = [].slice.call(arguments, 0);
+    models.forEach(function (model) {
+      model.__rendered = false;
+      (function (m) {
+        m.dispatch.on('renderEnd', function (arg) {
+          m.__rendered = true;
+          self.renderEnd('model');
+        });
+      })(model);
+
+      if (renderStack.indexOf(model) < 0) {
+        renderStack.push(model);
+      }
+    });
+    return this;
+  };
+
+  this.reset = function (duration) {
+    if (duration !== undefined) {
+      _duration = duration;
+    }
+    renderStack = [];
+  };
+
+  this.transition = function (selection, args, duration) {
+    args = arguments.length > 1 ? [].slice.call(arguments, 1) : [];
+
+    if (args.length > 1) {
+      duration = args.pop();
+    } else {
+      duration = _duration !== undefined ? _duration : 250;
+    }
+    selection.__rendered = false;
+
+    if (renderStack.indexOf(selection) < 0) {
+      renderStack.push(selection);
+    }
+
+    if (duration === 0) {
+      selection.__rendered = true;
+      selection.delay = function () {
+        return this;
+      };
+      selection.duration = function () {
+        return this;
+      };
+      return selection;
+    } else {
+      if (selection.length === 0) {
+        selection.__rendered = true;
+      } else if (selection.every(function (d) {
+          return !d.length;
+        })) {
+        selection.__rendered = true;
+      } else {
+        selection.__rendered = false;
+      }
+
+      var n = 0;
+      return selection
+        .transition()
+        .duration(duration)
+        .each(function () {
+          ++n;
+        })
+        .each('end', function (d, i) {
+          if (--n === 0) {
+            selection.__rendered = true;
+            self.renderEnd.apply(this, args);
+          }
+        });
+    }
+  };
+
+  this.renderEnd = function () {
+    if (renderStack.every(function (d) {
+        return d.__rendered;
+      })) {
+      renderStack.forEach(function (d) {
+        d.__rendered = false;
+      });
+      dispatch.renderEnd.apply(this, arguments);
+    }
+  }
+
+};
+
+
+/*
+ Takes multiple objects and combines them into the first one (dst)
+ example:  nv.utils.deepExtend({a: 1}, {a: 2, b: 3}, {c: 4});
+ gives:  {a: 2, b: 3, c: 4}
+ */
+nv.utils.deepExtend = function (dst) {
+  var sources = arguments.length > 1 ? [].slice.call(arguments, 1) : [];
+  sources.forEach(function (source) {
+    for (var key in source) {
+      var isArray = nv.utils.isArray(dst[key]);
+      var isObject = nv.utils.isObject(dst[key]);
+      var srcObj = nv.utils.isObject(source[key]);
+
+      if (isObject && !isArray && srcObj) {
+        nv.utils.deepExtend(dst[key], source[key]);
+      } else {
+        dst[key] = source[key];
+      }
+    }
+  });
+};
+
+
+/*
+ state utility object, used to track d3 states in the models
+ */
+nv.utils.state = function () {
+  if (!(this instanceof nv.utils.state)) {
+    return new nv.utils.state();
+  }
+  var state = {};
+  var _self = this;
+  var _setState = function () {
+  };
+  var _getState = function () {
+    return {};
+  };
+  var init = null;
+  var changed = null;
+
+  this.dispatch = d3.dispatch('change', 'set');
+
+  this.dispatch.on('set', function (state) {
+    _setState(state, true);
+  });
+
+  this.getter = function (fn) {
+    _getState = fn;
+    return this;
+  };
+
+  this.setter = function (fn, callback) {
+    if (!callback) {
+      callback = function () {
+      };
+    }
+    _setState = function (state, update) {
+      fn(state);
+      if (update) {
+        callback();
+      }
+    };
+    return this;
+  };
+
+  this.init = function (state) {
+    init = init || {};
+    nv.utils.deepExtend(init, state);
+  };
+
+  var _set = function () {
+    var settings = _getState();
+
+    if (JSON.stringify(settings) === JSON.stringify(state)) {
+      return false;
+    }
+
+    for (var key in settings) {
+      if (state[key] === undefined) {
+        state[key] = {};
+      }
+      state[key] = settings[key];
+      changed = true;
+    }
+    return true;
+  };
+
+  this.update = function () {
+    if (init) {
+      _setState(init, false);
+      init = null;
+    }
+    if (_set.call(this)) {
+      this.dispatch.change(state);
+    }
+  };
+
+};
+
+
+/*
+ Snippet of code you can insert into each nv.models.* to give you the ability to
+ do things like:
+ chart.options({
+ showXAxis: true,
+ tooltips: true
+ });
+
+ To enable in the chart:
+ chart.options = nv.utils.optionsFunc.bind(chart);
+ */
+nv.utils.optionsFunc = function (args) {
+  if (args) {
+    d3.map(args).forEach((function (key, value) {
+      if (nv.utils.isFunction(this[key])) {
+        this[key](value);
+      }
+    }).bind(this));
+  }
+  return this;
+};
+
+
+/*
+ numTicks:  requested number of ticks
+ data:  the chart data
+
+ returns the number of ticks to actually use on X axis, based on chart data
+ to avoid duplicate ticks with the same value
+ */
+nv.utils.calcTicksX = function (numTicks, data) {
+  // find max number of values from all data streams
+  var numValues = 1;
+  var i = 0;
+  for (i; i < data.length; i += 1) {
+    var stream_len = data[i] && data[i].values ? data[i].values.length : 0;
+    numValues = stream_len > numValues ? stream_len : numValues;
+  }
+  nv.log("Requested number of ticks: ", numTicks);
+  nv.log("Calculated max values to be: ", numValues);
+  // make sure we don't have more ticks than values to avoid duplicates
+  numTicks = numTicks > numValues ? numTicks = numValues - 1 : numTicks;
+  // make sure we have at least one tick
+  numTicks = numTicks < 1 ? 1 : numTicks;
+  // make sure it's an integer
+  numTicks = Math.floor(numTicks);
+  nv.log("Calculating tick count as: ", numTicks);
+  return numTicks;
+};
+
+
+/*
+ returns number of ticks to actually use on Y axis, based on chart data
+ */
+nv.utils.calcTicksY = function (numTicks, data) {
+  // currently uses the same logic but we can adjust here if needed later
+  return nv.utils.calcTicksX(numTicks, data);
+};
+
+
+/*
+ Add a particular option from an options object onto chart
+ Options exposed on a chart are a getter/setter function that returns chart
+ on set to mimic typical d3 option chaining, e.g. svg.option1('a').option2('b');
+
+ option objects should be generated via Object.create() to provide
+ the option of manipulating data via get/set functions.
+ */
+nv.utils.initOption = function (chart, name) {
+  // if it's a call option, just call it directly, otherwise do get/set
+  if (chart._calls && chart._calls[name]) {
+    chart[name] = chart._calls[name];
+  } else {
+    chart[name] = function (_) {
+      if (!arguments.length) return chart._options[name];
+      chart._overrides[name] = true;
+      chart._options[name] = _;
+      return chart;
+    };
+    // calling the option as _option will ignore if set by option already
+    // so nvd3 can set options internally but the stop if set manually
+    chart['_' + name] = function (_) {
+      if (!arguments.length) return chart._options[name];
+      if (!chart._overrides[name]) {
+        chart._options[name] = _;
+      }
+      return chart;
+    }
+  }
+};
+
+
+/*
+ Add all options in an options object to the chart
+ */
+nv.utils.initOptions = function (chart) {
+  chart._overrides = chart._overrides || {};
+  var ops = Object.getOwnPropertyNames(chart._options || {});
+  var calls = Object.getOwnPropertyNames(chart._calls || {});
+  ops = ops.concat(calls);
+  for (var i in ops) {
+    nv.utils.initOption(chart, ops[i]);
+  }
+};
+
+
+/*
+ Inherit options from a D3 object
+ d3.rebind makes calling the function on target actually call it on source
+ Also use _d3options so we can track what we inherit for documentation and chained inheritance
+ */
+nv.utils.inheritOptionsD3 = function (target, d3_source, oplist) {
+  target._d3options = oplist.concat(target._d3options || []);
+  oplist.unshift(d3_source);
+  oplist.unshift(target);
+  d3.rebind.apply(this, oplist);
+};
+
+
+/*
+ Remove duplicates from an array
+ */
+nv.utils.arrayUnique = function (a) {
+  return a.sort().filter(function (item, pos) {
+    return !pos || item != a[pos - 1];
+  });
+};
+
+
+/*
+ Keeps a list of custom symbols to draw from in addition to d3.svg.symbol
+ Necessary since d3 doesn't let you extend its list -_-
+ Add new symbols by doing nv.utils.symbols.set('name', function(size){...});
+ */
+nv.utils.symbolMap = d3.map();
+
+
+/*
+ Replaces d3.svg.symbol so that we can look both there and our own map
+ */
+nv.utils.symbol = function () {
+  var type,
+    size = 64;
+
+  function symbol(d, i) {
+    var t = type.call(this, d, i);
+    var s = size.call(this, d, i);
+    if (d3.svg.symbolTypes.indexOf(t) !== -1) {
+      return d3.svg.symbol().type(t).size(s)();
+    } else {
+      return nv.utils.symbolMap.get(t)(s);
+    }
+  }
+
+  symbol.type = function (_) {
+    if (!arguments.length) return type;
+    type = d3.functor(_);
+    return symbol;
+  };
+  symbol.size = function (_) {
+    if (!arguments.length) return size;
+    size = d3.functor(_);
+    return symbol;
+  };
+  return symbol;
+};
+
+
+/*
+ Inherit option getter/setter functions from source to target
+ d3.rebind makes calling the function on target actually call it on source
+ Also track via _inherited and _d3options so we can track what we inherit
+ for documentation generation purposes and chained inheritance
+ */
+nv.utils.inheritOptions = function (target, source) {
+  // inherit all the things
+  var ops = Object.getOwnPropertyNames(source._options || {});
+  var calls = Object.getOwnPropertyNames(source._calls || {});
+  var inherited = source._inherited || [];
+  var d3ops = source._d3options || [];
+  var args = ops.concat(calls).concat(inherited).concat(d3ops);
+  args.unshift(source);
+  args.unshift(target);
+  d3.rebind.apply(this, args);
+  // pass along the lists to keep track of them, don't allow duplicates
+  target._inherited = nv.utils.arrayUnique(ops.concat(calls).concat(inherited).concat(ops).concat(target._inherited || []));
+  target._d3options = nv.utils.arrayUnique(d3ops.concat(target._d3options || []));
+};
+
+
+/*
+ Runs common initialize code on the svg before the chart builds
+ */
+nv.utils.initSVG = function (svg) {
+  svg.classed({'nvd3-svg': true});
+};
+
+
+/*
+ Sanitize and provide default for the container height.
+ */
+nv.utils.sanitizeHeight = function (height, container) {
+  return (height || parseInt(container.style('height'), 10) || 400);
+};
+
+
+/*
+ Sanitize and provide default for the container width.
+ */
+nv.utils.sanitizeWidth = function (width, container) {
+  return (width || parseInt(container.style('width'), 10) || 960);
+};
+
+
+/*
+ Calculate the available height for a chart.
+ */
+nv.utils.availableHeight = function (height, container, margin) {
+  return Math.max(0, nv.utils.sanitizeHeight(height, container) - margin.top - margin.bottom);
+};
+
+/*
+ Calculate the available width for a chart.
+ */
+nv.utils.availableWidth = function (width, container, margin) {
+  return Math.max(0, nv.utils.sanitizeWidth(width, container) - margin.left - margin.right);
+};
+
+/*
+ Clear any rendered chart components and display a chart's 'noData' message
+ */
+nv.utils.noData = function (chart, container) {
+  var opt = chart.options(),
+    margin = opt.margin(),
+    noData = opt.noData(),
+    data = (noData == null) ? ["No Data Available."] : [noData],
+    height = nv.utils.availableHeight(null, container, margin),
+    width = nv.utils.availableWidth(null, container, margin),
+    x = margin.left + width / 2,
+    y = margin.top + height / 2;
+
+  //Remove any previously created chart components
+  container.selectAll('g').remove();
+
+  var noDataText = container.selectAll('.nv-noData').data(data);
+
+  noDataText.enter().append('text')
+    .attr('class', 'nvd3 nv-noData')
+    .attr('dy', '-.7em')
+    .style('text-anchor', 'middle');
+
+  noDataText
+    .attr('x', x)
+    .attr('y', y)
+    .text(function (t) {
+      return t;
+    });
+};
+
+/*
+ Wrap long labels.
+ */
+nv.utils.wrapTicks = function (text, width) {
+  text.each(function () {
+    var text = d3.select(this),
+      words = text.text().split(/\s+/).reverse(),
+      word,
+      line = [],
+      lineNumber = 0,
+      lineHeight = 1.1,
+      y = text.attr("y"),
+      dy = parseFloat(text.attr("dy")),
+      tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
+    while (word = words.pop()) {
+      line.push(word);
+      tspan.text(line.join(" "));
+      if (tspan.node().getComputedTextLength() > width) {
+        line.pop();
+        tspan.text(line.join(" "));
+        line = [word];
+        tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
+      }
+    }
+  });
+};
+
+/*
+ Check equality of 2 array
+ */
+nv.utils.arrayEquals = function (array1, array2) {
+  if (array1 === array2)
+    return true;
+
+  if (!array1 || !array2)
+    return false;
+
+  // compare lengths - can save a lot of time
+  if (array1.length != array2.length)
+    return false;
+
+  for (var i = 0,
+         l = array1.length; i < l; i++) {
+    // Check if we have nested arrays
+    if (array1[i] instanceof Array && array2[i] instanceof Array) {
+      // recurse into the nested arrays
+      if (!nv.arrayEquals(array1[i], array2[i]))
+        return false;
+    } else if (array1[i] != array2[i]) {
+      // Warning - two different object instances will never be equal: {x:20} != {x:20}
+      return false;
+    }
+  }
+  return true;
+};