Browse Source

HUE-2300 [search] Introduced partitioned tree

Enrico Berti 11 years ago
parent
commit
464f85f

+ 41 - 54
apps/search/src/search/templates/search.mako

@@ -20,7 +20,6 @@ from django.utils.translation import ugettext as _
 %>
 
 <%namespace name="dashboard" file="common_dashboard.mako" />
-<%namespace name="tree" file="common_tree.mako" />
 
 ${ commonheader(_('Search'), "search", user, "80px") | n,unicode }
 
@@ -876,7 +875,15 @@ ${ dashboard.layout_skeleton() }
       <div class="clearfix"></div>
 
       <!-- ko if: properties.scope() == 'tree' -->
-        ${ tree.render(id='expandableTree', data='treeDataTransformer($parent.count)', afterRender='treeAfterRender') }
+        <div data-bind="partitionChart: {datum: {counts: $parent.count, widget_id: id(), label: $parent.label},
+          fqs: $root.query.fqs,
+          transformer: partitionChartDataTransformer,
+          onStateChange: function(state){ },
+          onClick: function(d) {
+            $root.query.togglePivotFacet({facet: d.obj, widget_id: id()});
+          },
+          onComplete: function(){ viewModel.getWidgetById(id()).isLoading(false) } }"
+        />
       <!-- /ko -->
 
       <!-- ko if: properties.scope() == 'stack' -->
@@ -1197,8 +1204,6 @@ ${ dashboard.import_layout() }
 ${ dashboard.import_bindings() }
 ${ dashboard.import_charts() }
 
-${ tree.import_templates(itemClick='toggleTreeNode', iconClick='toggleTreeNode') }
-
 <script type="text/javascript" charset="utf-8">
 var viewModel;
 
@@ -1265,83 +1270,65 @@ function barChartDataTransformer(rawDatum) {
   return _datum;
 }
 
-
-function toggleTreeNode (obj) {
-  obj.isExpanded(!obj.isExpanded());
-}
-
-function treeDataTransformer(rawDatum) {
-
-  var _initialGrowingTree = {
-    path: "__HUEROOT__",
-    name: "__HUEROOT__",
-    selected: false,
-    nodes: [
-    ]
-  };
-
-
-  $(rawDatum).each(function (cnt, item) {
+function partitionChartDataTransformer(rawDatum) {
+  var _partitionData = {
+    name: "${ _('Total') }",
+    children: []
+  }
+  var _categories = [];
+  $(rawDatum.counts).each(function (cnt, item) {
     item.widget_id = rawDatum.widget_id;
 
-    var _cat = null;
-    _initialGrowingTree.nodes.forEach(function(cat){
-      if (cat.name == item.value){
-        _cat = cat;
+    var _category = null;
+    _categories.forEach(function (category) {
+      if (category.name == item.value) {
+        _category = category;
       }
     });
 
-    if (_cat == null){
-      _cat = {
-        path: item.value,
+    if (_category == null) {
+      _category = {
         name: item.value,
-        isExpanded: true,
-        selected: false,
-        nodes: []
+        children: []
       }
-     _initialGrowingTree.nodes.push(_cat);
+      _categories.push(_category);
     }
 
-    _cat.nodes.push({
-      path: item.cat,
-      name: item.cat + ' (' + item.count + ')',
-      isExpanded: true,
-      selected: false,
-      nodes: []
+    _category.children.push({
+      name: item.cat,
+      size: item.count,
+      obj: item,
+      children: []
     });
   });
 
-  var _growingTree = jQuery.extend(true, {}, _initialGrowingTree);
-
-  return new TreeNodeModel(_growingTree);
-}
-
-function treeAfterRender() {
+  _partitionData.children = _categories;
 
+  return _partitionData;
 }
 
 function pivotChartDataTransformer(rawDatum) {
-  var _cats = [];
+  var _categories = [];
 
   $(rawDatum.counts).each(function (cnt, item) {
     item.widget_id = rawDatum.widget_id;
 
-    var _cat = null;
-    _cats.forEach(function(cat){
-      if (cat.key == item.value){
-        _cat = cat;
+    var _category = null;
+    _categories.forEach(function (category) {
+      if (category.key == item.value) {
+        _category = category;
       }
     });
 
-    if (_cat == null){
-     _cat = {
+    if (_category == null) {
+      _category = {
         key: item.value,
         values: []
       };
-     _cats.push(_cat);
+      _categories.push(_category);
     }
 
-    _cat.values.push({
+    _category.values.push({
       series: 0,
       x: item.cat,
       y: item.count,
@@ -1349,7 +1336,7 @@ function pivotChartDataTransformer(rawDatum) {
     });
   });
 
-  return _cats;
+  return _categories;
 }
 
 function lineChartDataTransformer(rawDatum) {

+ 95 - 0
apps/search/static/css/search.css

@@ -386,4 +386,99 @@ em {
 
 .tree {
   margin: 20px;
+}
+
+.table tbody tr:hover > td,
+.table tbody tr:hover > th,
+.table tbody tr:hover td, .table tbody tr:hover th {
+  background-color: transparent!important;
+}
+
+.pivot-cell {
+  cursor: pointer;
+}
+
+.pivot-cell.selected {
+  border: 2px dashed #CCC;
+}
+
+.partitionChart {
+  display: block;
+  margin: auto;
+  margin-top: 10px;
+  font-size: 11px;
+}
+
+.partitionChart rect {
+  fill-opacity: .8;
+}
+
+.partitionChart rect.mouseover {
+  fill-opacity: 1!important;
+}
+
+.partitionChart rect.parent {
+  cursor: pointer;
+}
+
+.partitionChart text {
+  pointer-events: none;
+  fill: white;
+}
+
+.d3-tip {
+  line-height: 1;
+  font-weight: bold;
+  padding: 12px;
+  background: rgba(0, 0, 0, 0.8);
+  color: #fff;
+  border-radius: 2px;
+  pointer-events: none;
+  margin-top: 5px;
+}
+
+/* Creates a small triangle extender for the tooltip */
+.d3-tip:after {
+  box-sizing: border-box;
+  display: inline;
+  font-size: 10px;
+  width: 100%;
+  line-height: 1;
+  color: rgba(0, 0, 0, 0.8);
+  position: absolute;
+  pointer-events: none;
+}
+
+/* Northward tooltips */
+.d3-tip.n:after {
+  content: "\25BC";
+  margin: -3px 0 0 0;
+  top: 100%;
+  left: 0;
+  text-align: center;
+}
+
+/* Eastward tooltips */
+.d3-tip.e:after {
+  content: "\25C0";
+  margin: -4px 0 0 0;
+  top: 50%;
+  left: -8px;
+}
+
+/* Southward tooltips */
+.d3-tip.s:after {
+  content: "\25B2";
+  margin: 0 0 1px 0;
+  top: -8px;
+  left: 0;
+  text-align: center;
+}
+
+/* Westward tooltips */
+.d3-tip.w:after {
+  content: "\25B6";
+  margin: -4px 0 0 -1px;
+  top: 50%;
+  left: 100%;
 }

+ 464 - 29
desktop/core/static/js/ko.charts.js

@@ -35,13 +35,13 @@ ko.bindingHandlers.pieChart = {
       $(element).width(Math.min($(element).parent().width(), _max));
     }
 
-    if ($(element).find('svg').length > 0 && _data.length == 0) {
-      $(element).find('svg').empty();
+    if ($(element).find("svg").length > 0 && _data.length == 0) {
+      $(element).find("svg").empty();
     }
 
     if (_data.length > 0 && isNaN(_data[0].value)) {
       _data = [];
-      $(element).find('svg').empty();
+      $(element).find("svg").empty();
     }
 
     if ($(element).is(":visible")) {
@@ -56,7 +56,7 @@ ko.bindingHandlers.pieChart = {
             .height($(element).width())
             .showLabels(true).showLegend(false);
 
-        var _d3 = ($(element).find('svg').length > 0) ? d3.select($(element).find('svg')[0]) : d3.select($(element)[0]).append('svg');
+        var _d3 = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
 
         _d3.datum(_data)
             .transition().duration(150)
@@ -83,8 +83,8 @@ ko.bindingHandlers.pieChart = {
 
         return _chart;
       }, function () {
-        var _d3 = ($(element).find('svg').length > 0) ? d3.select($(element).find('svg')[0]) : d3.select($(element)[0]).append('svg');
-        _d3.selectAll(".nv-slice").on('click',
+        var _d3 = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
+        _d3.selectAll(".nv-slice").on("click",
             function (d, i) {
               if (typeof _options.onClick != "undefined") {
                 chartsUpdatingState();
@@ -177,7 +177,7 @@ ko.bindingHandlers.leafletMapChart = {
         if (_map == null) {
           _map = L.map(element);
           L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
-            attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributorss'
+            attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
           }).addTo(_map);
         }
 
@@ -370,12 +370,12 @@ ko.bindingHandlers.mapChart = {
 function lineChartBuilder(element, options) {
   var _datum = options.transformer(options.datum);
   $(element).height(300);
-  if ($(element).find('svg').length > 0 && (_datum.length == 0 || _datum[0].values.length == 0)) {
-    $(element).find('svg').empty();
+  if ($(element).find("svg").length > 0 && (_datum.length == 0 || _datum[0].values.length == 0)) {
+    $(element).find("svg").empty();
   }
   if (_datum.length > 0 && _datum[0].values.length > 0 && (isNaN(_datum[0].values[0].x) || isNaN(_datum[0].values[0].y))) {
     _datum = [];
-    $(element).find('svg').empty();
+    $(element).find("svg").empty();
   }
 
   if ($(element).is(":visible")) {
@@ -394,9 +394,9 @@ function lineChartBuilder(element, options) {
       _chart.xAxis.showMaxMin(false);
 
       _chart.yAxis
-          .tickFormat(d3.format(',0f'));
+          .tickFormat(d3.format(",0f"));
 
-      var _d3 = ($(element).find('svg').length > 0) ? d3.select($(element).find('svg')[0]) : d3.select($(element)[0]).append('svg');
+      var _d3 = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
       _d3.datum(_datum)
           .transition().duration(150)
           .each("end", options.onComplete != null ? options.onComplete : void(0))
@@ -406,8 +406,8 @@ function lineChartBuilder(element, options) {
 
       return _chart;
     }, function () {
-      var _d3 = ($(element).find('svg').length > 0) ? d3.select($(element).find('svg')[0]) : d3.select($(element)[0]).append('svg');
-      _d3.selectAll(".nv-line").on('click',
+      var _d3 = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
+      _d3.selectAll(".nv-line").on("click",
           function (d, i) {
             if (typeof options.onClick != "undefined") {
               chartsUpdatingState();
@@ -425,13 +425,13 @@ function barChartBuilder(element, options, isTimeline) {
 
   var _isPivot = options.isPivot != null ? options.isPivot : false;
 
-  if ($(element).find('svg').length > 0 && (_datum.length == 0 || _datum[0].values.length == 0)) {
-    $(element).find('svg').empty();
+  if ($(element).find("svg").length > 0 && (_datum.length == 0 || _datum[0].values.length == 0)) {
+    $(element).find("svg").empty();
   }
 
   if (_datum.length > 0 && _datum[0].values.length > 0 && isNaN(_datum[0].values[0].y)) {
     _datum = [];
-    $(element).find('svg').empty();
+    $(element).find("svg").empty();
   }
 
   if ($(element).is(":visible")) {
@@ -444,19 +444,19 @@ function barChartBuilder(element, options, isTimeline) {
         var _mom = moment(d);
         if (_mom != null && _mom.isValid()) {
           var _words = _mom.format("hh:mm:ss YYYY-MM-DD").split(" ");
-          _el.text('');
+          _el.text("");
           for (var i = 0; i < _words.length; i++) {
             var tspan = _el.append("tspan").text(_words[i]);
             if (i > 0) {
-              tspan.attr("x", 0).attr("dy", '15');
+              tspan.attr("x", 0).attr("dy", "15");
             }
           }
         }
       };
 
       if (isTimeline) {
-        if ($(element).find('svg').length > 0 && $(element).find('.nv-discreteBarWithAxes').length > 0) {
-          $(element).find('svg').empty();
+        if ($(element).find("svg").length > 0 && $(element).find(".nv-discreteBarWithAxes").length > 0) {
+          $(element).find("svg").empty();
         }
         _chart = nv.models.multiBarWithBrushChart();
         if (_datum.length > 0 && _datum[0].values.length > 10) {
@@ -485,8 +485,8 @@ function barChartBuilder(element, options, isTimeline) {
           }
         }
         if (_isDiscrete && ! _isPivot) {
-          if ($(element).find('svg').length > 0 && $(element).find('.nv-multiBarWithLegend').length > 0) {
-            $(element).find('svg').empty();
+          if ($(element).find("svg").length > 0 && $(element).find(".nv-multiBarWithLegend").length > 0) {
+            $(element).find("svg").empty();
           }
           _chart = nv.models.growingDiscreteBarChart()
               .x(function (d) {
@@ -498,8 +498,8 @@ function barChartBuilder(element, options, isTimeline) {
               .staggerLabels(true);
         }
         else {
-          if ($(element).find('svg').length > 0 && $(element).find('.nv-discreteBarWithAxes').length > 0) {
-            $(element).find('svg').empty();
+          if ($(element).find("svg").length > 0 && $(element).find(".nv-discreteBarWithAxes").length > 0) {
+            $(element).find("svg").empty();
           }
           _chart = nv.models.multiBarWithBrushChart();
           if (_datum.length > 0 && _datum[0].values.length > 10) {
@@ -510,7 +510,7 @@ function barChartBuilder(element, options, isTimeline) {
             _chart.hideSelection();
           }
           else {
-            _chart.xAxis.showMaxMin(false).tickFormat(d3.format(',0f'));
+            _chart.xAxis.showMaxMin(false).tickFormat(d3.format(",0f"));
           }
           _chart.multibar.hideable(true);
           _chart.multibar.stacked(typeof options.stacked != "undefined" ? options.stacked : false);
@@ -524,9 +524,9 @@ function barChartBuilder(element, options, isTimeline) {
       _chart.transitionDuration(0);
 
       _chart.yAxis
-          .tickFormat(d3.format(',0f'));
+          .tickFormat(d3.format(",0f"));
 
-      var _d3 = ($(element).find('svg').length > 0) ? d3.select($(element).find('svg')[0]) : d3.select($(element)[0]).append('svg');
+      var _d3 = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
       _d3.datum(_datum)
           .transition().duration(150)
           .each("end", function () {
@@ -554,7 +554,7 @@ function barChartBuilder(element, options, isTimeline) {
 
       return _chart;
     }, function () {
-      var _d3 = ($(element).find('svg').length > 0) ? d3.select($(element).find('svg')[0]) : d3.select($(element)[0]).append('svg');
+      var _d3 = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
       _d3.selectAll(".nv-bar").on("click",
           function (d, i) {
             if (typeof options.onClick != "undefined") {
@@ -566,6 +566,441 @@ function barChartBuilder(element, options, isTimeline) {
   }
 }
 
+ko.bindingHandlers.partitionChart = {
+  update: function (element, valueAccessor) {
+
+    var MIN_HEIGHT_FOR_TOOLTIP = 24;
+
+    var _options = valueAccessor();
+    var _data = _options.transformer(valueAccessor().datum);
+
+    var _w = $(element).width(),
+        _h = 300,
+        _x = d3.scale.linear().range([0, _w]),
+        _y = d3.scale.linear().range([0, _h]);
+
+    var _tip = d3.tip()
+        .attr("class", "d3-tip")
+        .html(function (d) {
+          if (d.depth < 2) {
+            return d.name + " (" + d.value + ")";
+          }
+          else {
+            return d.parent.name + " - " + d.name + " (" + d.value + ")";
+          }
+        })
+        .offset([-12, 0])
+
+
+    var _svg = d3.select(element).append("svg:svg");
+    _svg.call(_tip);
+
+
+    var _vis = ($(element).find("svg").length > 0) ? d3.select($(element).find("svg")[0]) : d3.select($(element)[0]).append("svg");
+    _vis.attr("class", "partitionChart")
+        .style("width", _w + "px")
+        .style("height", _h + "px")
+        .append("svg:svg")
+        .attr("width", _w)
+        .attr("height", _h);
+
+    var _partition = d3.layout.partition()
+        .value(function (d) {
+          return d.size;
+        });
+
+    var g = _vis.selectAll("g")
+        .data(_partition.nodes(_data))
+        .enter().append("svg:g")
+        .attr("transform", function (d) {
+          return "translate(" + _x(d.y) + "," + _y(d.x) + ")";
+        })
+        .on("click", click)
+        .on("dblclick", function (d, i) {
+          if (typeof _options.onClick != "undefined") {
+            chartsUpdatingState();
+            _options.onClick(d);
+          }
+        })
+        .on("mouseover", function (d, i) {
+          if (element.querySelectorAll("rect")[i].getBBox().height < MIN_HEIGHT_FOR_TOOLTIP) {
+            _tip.attr("class", "d3-tip").show(d);
+          }
+
+          if (this.__data__.parent == undefined) return;
+          d3.select(this).select("rect").classed("mouseover", true)
+        })
+        .on("mouseout", function (d, i) {
+          if (element.querySelectorAll("rect")[i].getBBox().height < MIN_HEIGHT_FOR_TOOLTIP) {
+            _tip.attr("class", "d3-tip").show(d);
+            _tip.hide();
+          }
+          d3.select(this).select("rect").classed("mouseover", false)
+        });
+
+    var _kx = _w / _data.dx,
+        _ky = _h / 1;
+
+    var _colors = HueColors.scale(HueColors.DARK_BLUE, HueColors.BLUE, 5)
+
+    g.append("svg:rect")
+        .attr("width", _data.dy * _kx)
+        .attr("height", function (d) {
+          return d.dx * _ky;
+        })
+        .attr("class", function (d) {
+          return d.children ? "parent" : "child";
+        })
+        .attr("stroke", HueColors.DARK_BLUE)
+        .attr("fill", function (d, i) {
+          return _colors[d.depth];
+        });
+
+
+    g.append("svg:text")
+        .attr("transform", transform)
+        .attr("dy", ".35em")
+        .style("opacity", function (d) {
+          return d.dx * _ky > 12 ? 1 : 0;
+        })
+        .text(function (d) {
+          if (d.depth < 2) {
+            return d.name + " (" + d.value + ")";
+          }
+          else {
+            return d.parent.name + " - " + d.name + " (" + d.value + ")";
+          }
+        });
+
+    d3.select(window)
+        .on("click", function () {
+          click(_data);
+        });
+
+    function click(d) {
+      _tip.hide();
+      if (!d.children) return;
+
+      _kx = (d.y ? _w - 40 : _w) / (1 - d.y);
+      _ky = _h / d.dx;
+      _x.domain([d.y, 1]).range([d.y ? 40 : 0, _w]);
+      _y.domain([d.x, d.x + d.dx]);
+
+      var t = g.transition()
+          .duration(d3.event.altKey ? 7500 : 750)
+          .attr("transform", function (d) {
+            return "translate(" + _x(d.y) + "," + _y(d.x) + ")";
+          });
+
+      t.select("rect")
+          .attr("width", d.dy * _kx)
+          .attr("height", function (d) {
+            return d.dx * _ky;
+          });
+
+      t.select("text")
+          .attr("transform", transform)
+          .style("opacity", function (d) {
+            return d.dx * _ky > 12 ? 1 : 0;
+          });
+
+      d3.event.stopPropagation();
+    }
+
+    function transform(d) {
+      return "translate(8," + d.dx * _ky / 2 + ")";
+    }
+
+  }
+};
+
+
 function chartsUpdatingState() {
   $(document).find("svg").css("opacity", "0.5");
 }
+
+
+var tipBuilder = function () {
+  var direction = d3_tip_direction,
+      offset = d3_tip_offset,
+      html = d3_tip_html,
+      node = initNode(),
+      svg = null,
+      point = null,
+      target = null
+
+  function tip(vis) {
+    svg = getSVGNode(vis)
+    point = svg.createSVGPoint()
+    document.body.appendChild(node)
+  }
+
+  // Public - show the tooltip on the screen
+  //
+  // Returns a tip
+  tip.show = function () {
+    var args = Array.prototype.slice.call(arguments)
+    if (args[args.length - 1] instanceof SVGElement) target = args.pop()
+
+    var content = html.apply(this, args),
+        poffset = offset.apply(this, args),
+        dir = direction.apply(this, args),
+        nodel = d3.select(node), i = 0,
+        coords
+
+    nodel.html(content)
+        .style({ opacity: 1, "pointer-events": "all" })
+
+    while (i--) nodel.classed(directions[i], false)
+    coords = direction_callbacks.get(dir).apply(this)
+    nodel.classed(dir, true).style({
+      top: (coords.top + poffset[0]) + "px",
+      left: (coords.left + poffset[1]) + "px"
+    })
+
+    return tip
+  }
+
+  // Public - hide the tooltip
+  //
+  // Returns a tip
+  tip.hide = function () {
+    nodel = d3.select(node)
+    nodel.style({ opacity: 0, "pointer-events": "none" })
+    return tip
+  }
+
+  // Public: Proxy attr calls to the d3 tip container.  Sets or gets attribute value.
+  //
+  // n - name of the attribute
+  // v - value of the attribute
+  //
+  // Returns tip or attribute value
+  tip.attr = function (n, v) {
+    if (arguments.length < 2 && typeof n === "string") {
+      return d3.select(node).attr(n)
+    } else {
+      var args = Array.prototype.slice.call(arguments)
+      d3.selection.prototype.attr.apply(d3.select(node), args)
+    }
+
+    return tip
+  }
+
+  // Public: Proxy style calls to the d3 tip container.  Sets or gets a style value.
+  //
+  // n - name of the property
+  // v - value of the property
+  //
+  // Returns tip or style property value
+  tip.style = function (n, v) {
+    if (arguments.length < 2 && typeof n === "string") {
+      return d3.select(node).style(n)
+    } else {
+      var args = Array.prototype.slice.call(arguments)
+      d3.selection.prototype.style.apply(d3.select(node), args)
+    }
+
+    return tip
+  }
+
+  // Public: Set or get the direction of the tooltip
+  //
+  // v - One of n(north), s(south), e(east), or w(west), nw(northwest),
+  //     sw(southwest), ne(northeast) or se(southeast)
+  //
+  // Returns tip or direction
+  tip.direction = function (v) {
+    if (!arguments.length) return direction
+    direction = v == null ? v : d3.functor(v)
+
+    return tip
+  }
+
+  // Public: Sets or gets the offset of the tip
+  //
+  // v - Array of [x, y] offset
+  //
+  // Returns offset or
+  tip.offset = function (v) {
+    if (!arguments.length) return offset
+    offset = v == null ? v : d3.functor(v)
+
+    return tip
+  }
+
+  // Public: sets or gets the html value of the tooltip
+  //
+  // v - String value of the tip
+  //
+  // Returns html value or tip
+  tip.html = function (v) {
+    if (!arguments.length) return html
+    html = v == null ? v : d3.functor(v)
+
+    return tip
+  }
+
+  function d3_tip_direction() {
+    return "n"
+  }
+
+  function d3_tip_offset() {
+    return [0, 0]
+  }
+
+  function d3_tip_html() {
+    return " "
+  }
+
+  var direction_callbacks = d3.map({
+        n: direction_n,
+        s: direction_s,
+        e: direction_e,
+        w: direction_w,
+        nw: direction_nw,
+        ne: direction_ne,
+        sw: direction_sw,
+        se: direction_se
+      }),
+
+      directions = direction_callbacks.keys()
+
+  function direction_n() {
+    var bbox = getScreenBBox()
+    return {
+      top: bbox.n.y - node.offsetHeight,
+      left: bbox.n.x - node.offsetWidth / 2
+    }
+  }
+
+  function direction_s() {
+    var bbox = getScreenBBox()
+    return {
+      top: bbox.s.y,
+      left: bbox.s.x - node.offsetWidth / 2
+    }
+  }
+
+  function direction_e() {
+    var bbox = getScreenBBox()
+    return {
+      top: bbox.e.y - node.offsetHeight / 2,
+      left: bbox.e.x
+    }
+  }
+
+  function direction_w() {
+    var bbox = getScreenBBox()
+    return {
+      top: bbox.w.y - node.offsetHeight / 2,
+      left: bbox.w.x - node.offsetWidth
+    }
+  }
+
+  function direction_nw() {
+    var bbox = getScreenBBox()
+    return {
+      top: bbox.nw.y - node.offsetHeight,
+      left: bbox.nw.x - node.offsetWidth
+    }
+  }
+
+  function direction_ne() {
+    var bbox = getScreenBBox()
+    return {
+      top: bbox.ne.y - node.offsetHeight,
+      left: bbox.ne.x
+    }
+  }
+
+  function direction_sw() {
+    var bbox = getScreenBBox()
+    return {
+      top: bbox.sw.y,
+      left: bbox.sw.x - node.offsetWidth
+    }
+  }
+
+  function direction_se() {
+    var bbox = getScreenBBox()
+    return {
+      top: bbox.se.y,
+      left: bbox.e.x
+    }
+  }
+
+  function initNode() {
+    var node = d3.select(document.createElement("div"))
+    node.style({
+      position: "absolute",
+      opacity: 0,
+      pointerEvents: "none",
+      boxSizing: "border-box"
+    })
+
+    return node.node()
+  }
+
+  function getSVGNode(el) {
+    el = el.node()
+    if (el != null) {
+      if (el.tagName != null && el.tagName.toLowerCase() == "svg")
+        return el
+
+      return el.ownerSVGElement
+    }
+  }
+
+  // Private - gets the screen coordinates of a shape
+  //
+  // Given a shape on the screen, will return an SVGPoint for the directions
+  // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
+  // sw(southwest).
+  //
+  //    +-+-+
+  //    |   |
+  //    +   +
+  //    |   |
+  //    +-+-+
+  //
+  // Returns an Object {n, s, e, w, nw, sw, ne, se}
+  function getScreenBBox() {
+    var targetel = target || d3.event.target,
+        bbox = {},
+        matrix = targetel.getScreenCTM(),
+        tbbox = targetel.getBBox(),
+        width = tbbox.width,
+        height = tbbox.height,
+        x = tbbox.x,
+        y = tbbox.y,
+        scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
+        scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
+
+
+    point.x = x + scrollLeft
+    point.y = y + scrollTop
+    bbox.nw = point.matrixTransform(matrix)
+    point.x += width
+    bbox.ne = point.matrixTransform(matrix)
+    point.y += height
+    bbox.se = point.matrixTransform(matrix)
+    point.x -= width
+    bbox.sw = point.matrixTransform(matrix)
+    point.y -= height / 2
+    bbox.w = point.matrixTransform(matrix)
+    point.x += width
+    bbox.e = point.matrixTransform(matrix)
+    point.x -= width / 2
+    point.y -= height / 2
+    bbox.n = point.matrixTransform(matrix)
+    point.y += height
+    bbox.s = point.matrixTransform(matrix)
+
+    return bbox
+  }
+
+  return tip
+};
+
+d3.tip = tipBuilder;