/**
  * Transform the container view center on (x,y) with a scale.
  * @param x coordinate
  * @param y coordinate
  * @param scale
  */
 function transform(x, y, scale) {
   var translate = [width / 2 - scale * x, height / 2 - scale * y];
   container.transition(transition().duration(750))
     .attr("transform", "translate(" + translate + ")scale(" + scale + ")")
     .selectAll(".label")
     .style("font-size", (15 / scale) + "px")
     .on("start", function (d) { return d.parent !== focus || d.parent === null ? this.style.display = "none" : undefined; })
     .on("end", function (d) { return d.parent === focus ? this.style.display = "inline" : undefined; });
 }
Esempio n. 2
0
 // Animation on update
 // This method is not called for the initial render
 // It also seems that this method is called several times
 // during an animation, so you should always test your props
 // before doing something with this one
 componentWillReceiveProps(nextProps) {
     if(!_.isEqual(this.props, nextProps)){
         select(findDOMNode(this))
             .transition(transition().duration(750))
             .attr('x', nextProps.i * 32)
             .style('fill', 'blue')
             .on('end', () => {
                 this.setState({
                     x: nextProps.i * 32,
                     fill: 'blue'
                 });
             });
     }
 }
Esempio n. 3
0
 // Animation on enter:
 // called at the same time as componentDidMount,
 // it means your element is already on the dom
 componentWillEnter(callback) {
     this.setState({
         x: this.props.i * 32
     });
     select(findDOMNode(this))
         .transition(transition().duration(750))
         .attr('y', 0)
         .style('fill-opacity', 1)
         .on('end', () => {
             this.setState({
                 y: 0,
                 fillOpacity: 1
             });
             callback();
         });
 }
        it('should use implicit rather than explicit transition', (done) => {
            join.transition(transition().duration(timeout * 10).ease(easeLinear));
            container = container.selection();

            const update = join(container, data);
            const node = update.enter().node();

            expect(node.style.opacity).toBeCloseTo(0.000001, 6);
            expect(node.parentNode).not.toBe(null);

            setTimeout(() => {
                expect(node.style.opacity).not.toBe('1');
                expect(node.parentNode).not.toBe(null);
                done();
            }, timeout);
        });
Esempio n. 5
0
    // Animation on exit
    componentWillLeave(callback) {
        this.setState({
            fill: 'red'
        });

        select(findDOMNode(this))
            .transition(transition().duration(750))
            .attr('y', 100)
            .style('fill-opacity', 0)
            .style('fill', 'red')
            .on('end', () => {
                this.setState({
                    y: 100,
                    fillOpacity: .1,
                });
                callback();
            });
    }
Esempio n. 6
0
  constructor() {
    // You have to call the super() method to initialize the base class.
    super();

    // Stores the currently displayed data so view can be reflown during transitions
    this._data = null;
    this._sizeClass = null;

    this._pie = null;
    this._stack = null;

    this.currencySymbols = {
      BRL: 'R$',
      EUR: '€',
      GBP: '£',
      USD: '$'
    };

    this.fill = {
      uber: {
        color: presentation10.standard[presentation10.names.blue],
        pattern: this._getPattern(presentation10.names.blue)
      },
      hailo: {
        color: presentation10.standard[presentation10.names.yellow],
        pattern: this._getPattern(presentation10.names.yellow)
      },
      addisonlee: {
        color: presentation10.standard[presentation10.names.grey],
        pattern: this._getPattern(presentation10.names.grey)
      }
    };

    // TODO: remove once the build pipeline is fixed
    transition();

    // We subscribe to 'storageupdate' updates from the Controller
    this.controller.subscribe('storageupdate', this.onStorageUpdate.bind(this));

    // The SiftView provides lifecycle methods for when the Sift is fully loaded into the DOM (onLoad)
    // NOTE: the registration of these methods will be handled in the SiftView base class and do not have to be
    // called by the developer manually in the next version.
    this.registerOnLoadHandler(this.onLoad.bind(this)); // FIXXME: expose as 'onLoad'
  }
Esempio n. 7
0
  didReceiveAttrs() {
    // Schedule a call to our `drawCircles` method on Ember's "render" queue, which will
    // happen after the component has been placed in the DOM, and subsequently
    // each time data is changed.
    run.scheduleOnce('render', this, this.drawCircles)
  },

  drawCircles() {
    let plot = select(this.element)
    let data = get(this, 'data')
    let width = get(this, 'width')
    let height = get(this, 'height')

    // Create a transition to use later
    let t = transition()
      .duration(250)
      .ease(easeCubicInOut)

    // X scale to scale position on x axis
    let xScale = scaleLinear()
      .domain(extent(data.map(d => d.timestamp)))
      .range([0, width])

    // Y scale to scale radius of circles proportional to size of plot
    let yScale = scaleLinear()
      .domain(
        // `extent()` requires that data is sorted ascending
        extent(data.map(d => d.value).sort(ascending))
      )
      .range([0, height])
Esempio n. 8
0
function render(element, state) {
    if (!defined(state.data)) {
        return;
    }
    const margin = defaultValue(state.margin, defaultMargin);
    const allUnits = getUniqueValues(state.data.map(line => line.units));
    const size = Size.calculate(element, margin, state, state.mini ? 1 : allUnits.length);
    const scales = Scales.calculate(size, state.domain, state.data);
    const data = state.data;
    const transitionDuration = defaultValue(state.transitionDuration, defaultTransitionDuration);
    // The last condition in hasData checks that at least one y-value of one chart is defined.
    const hasData = (data.length > 0 && data[0].points.length > 0 && data.some(d=>d.points.some(p=>defined(p.y))));

    const d3Element = d3Select(element);
    const svg = d3Element.select('svg')
        .attr('width', state.width)
        .attr('height', state.height);

    const g = d3Select(element).selectAll('.line-chart');

    const t = d3Transition().duration(transitionDuration);

    // Some axis animations need to be a little different if this is the first time we are showing the graph.
    // Assume it's the first time if we have no lines yet.
    const lines = g.selectAll('.line').data(data, d => d.id).attr('class', 'line');
    const isFirstLine = (!defined(lines.nodes()[0]));
    const gTransform = 'translate(' + (margin.left + size.yAxesWidth) + ',' + (margin.top + Title.getHeight(state.titleSettings)) + ')';
    if (isFirstLine) {
        g.attr('transform', gTransform);
    } else {
        g.transition(t)
            .attr('transform', gTransform);
    }

    const plotArea = d3Element.select('rect.plot-area');
    plotArea.attr('width', size.width)
        .attr('height', size.plotHeight);

    // Returns a path function which can be called with an array of points.
    function getPathForUnits(units) {
        return d3Line()
            // .curve(d3Shape.curveBasis)
            .x(d => scales.x(d.x))
            .y(d => scales.y[units](d.y));
            // NOTE: it was originally 'basic', which is not a interpolation
    }

    // Enter.
    // https://github.com/d3/d3/blob/master/CHANGES.md#selections-d3-selection
    // If there are undefined or null y-values, just ignore them. This works well for initial and final undefined values,
    // and simply interpolates over intermediate ones. This may not be what we want.
    lines.enter().append('path')
        .attr('class', 'line')
        .attr('d', line => getPathForUnits(line.units || Scales.unknownUnits)(line.points.filter(point => defined(point.y))))
        .style('fill', 'none')
        .style('opacity', 1e-6)
        .transition(t)
        .style('opacity', 1)
        .style('stroke', d => defined(d.color) ? d.color : '');

    // Mouse event.
    lines
        .on('mouseover', fade(g, 0.33))
        .on('mouseout', fade(g, 1));

    // Update.
    // Same approach to undefined or null y-values as enter.
    lines
        .transition(t)
        .attr('d', line => getPathForUnits(line.units || Scales.unknownUnits)(line.points.filter(point => defined(point.y))))
        .style('opacity', 1)
        .style('stroke', d => defined(d.color) ? d.color : '');

    // Exit.
    lines.exit()
        .transition(t)
        .style('opacity', 1e-6)
        .remove();

    // Title.
    Title.enterUpdateAndExit(d3Element, state.titleSettings, margin, data, transitionDuration);

    // Hilighted data and tooltips.
    if (defined(state.tooltipSettings)) {
        const tooltip = Tooltip.select(state.tooltipSettings);
        // Whenever the chart updates, remove the hilighted points and tooltips.
        const boundHilightDataAndShowTooltip = highlightDataAndShowTooltip.bind(null, hasData, data, state, scales, g, tooltip);
        unhilightDataAndHideTooltip(g, tooltip);
        svg.on('mouseover', boundHilightDataAndShowTooltip)
            .on('mousemove', boundHilightDataAndShowTooltip)
            .on('click', boundHilightDataAndShowTooltip)
            .on('mouseout', unhilightDataAndHideTooltip.bind(null, g, tooltip));
    }

    if (defined(state.highlightX)) {
        const selectedData = findSelectedData(data, state.highlightX);
        hilightData(selectedData, scales, g);
    }

    // Create the y-axes as needed.
    const yAxisElements = g.select('.y.axes').selectAll('.y.axis').data(allUnits, d => d);
    const newYAxisElements = yAxisElements.enter().append('g')
        .attr('class', 'y axis')
        .style('opacity', 1e-6);

    newYAxisElements.append('g')
        .attr('class', 'ticks');
    newYAxisElements.append('g')
        .attr('class', 'colors');
    newYAxisElements.append('g')
        .attr('class', 'units-label-shadow-group') // This doesn't yet do what we want, because it comes before the ticks.
        .attr('transform', 'translate(' + -(Size.yAxisWidth - yAxisLabelWidth) + ',6)rotate(270)')
            .append('text')
            .attr('class', 'units-label-shadow')
            .style('text-anchor', 'end');
    newYAxisElements.append('g')
        .attr('class', 'units-label-group')
        .attr('transform', 'translate(' + -(Size.yAxisWidth - yAxisLabelWidth) + ',6)rotate(270)')
            .append('text')
            .attr('class', 'units-label')
            .style('text-anchor', 'end');
    yAxisElements.exit().remove();

    // No data. Show message if no data to show.
    var noData = g.select('.no-data')
        .style('opacity', hasData ? 1e-6 : 1);

    noData.select('text')
        .text(defaultNoDataText)
        .style('text-anchor', 'middle')
        .attr('x', element.offsetWidth / 2 - margin.left - size.yAxesWidth)
        .attr('y', (size.height - 24) / 2);

    // Axes.
    if (!defined(scales)) {
        return;
    }

    // An extra calculation to decide whether we want the last automatically-generated tick value.
    const xTickValues = Scales.truncatedTickValues(scales.x, Math.min(12, Math.floor(size.width / 150) + 1));
    const xAxis = d3AxisBottom()
        .tickValues(xTickValues);
    if (defined(state.grid) && state.grid.x) {
        // Note this only extends up; if the axis is not at the bottom, we need to translate the ticks down too.
        xAxis.tickSizeInner(-size.plotHeight);
    }

    const yAxis = d3AxisLeft()
        .tickSizeOuter((allUnits.length > 1) ? 0 : 3);
    let y0 = 0;
    let mainYScale;
    if (defined(scales)) {
        mainYScale = scales.y[allUnits[0] || Scales.unknownUnits];
        xAxis.scale(scales.x);

        y0 = Math.min(Math.max(mainYScale(0), 0), size.plotHeight);
    }
    // Mini charts have the x-axis label at the bottom, regardless of where the x-axis would actually be.
    if (state.mini) {
        y0 = size.plotHeight;
    }
    // If this is the first line, start the x-axis in the right place straight away.
    if (isFirstLine) {
        g.select('.x.axis').attr('transform', 'translate(0,' + y0 + ')');
    }

    g.select('.x.axis')
        .transition(t)
        .attr('transform', 'translate(0,' + y0 + ')')
        .style('opacity', 1)
        .call(xAxis);
    if (defined(state.grid) && state.grid.x) {
        // Recall the x-axis-grid lines only extended up; we need to translate the ticks down to the bottom of the plot.
        g.selectAll('.x.axis line').attr('transform', 'translate(0,' + (size.plotHeight - y0) + ')');
    }
    // If mini with label, or no data: hide the ticks, but not the axis, so the x-axis label can still be shown.
    var hasXLabel = defined(state.axisLabel) && defined(state.axisLabel.x);
    g.select('.x.axis')
        .selectAll('.tick')
        .transition(t)
        .style('opacity', ((state.mini && hasXLabel) || !hasData) ? 1e-6 : 1);

    if (hasXLabel) {
        g.select('.x.axis .label')
            .style('text-anchor', 'middle')
            .text(state.axisLabel.x)
            .style('opacity', hasData ? 1 : 1e-6)
                // Translate the x-axis-label to the bottom of the plot, even if the x-axis itself is in the middle or the top.
                .attr('transform', 'translate(' + (size.width / 2) + ', ' + (size.height - y0) + ')');
    }

    if (state.mini) {
        yAxis.tickSize(0, 0);
    }
    const yAxisElementsElements = g.select('.y.axes').selectAll('.y.axis');

    yAxisElementsElements
        .transition(t)
        .attr('transform', (d, i) => ('translate(' + (- i * Size.yAxisWidth) + ', 0)'));

    yAxisElementsElements.nodes().forEach(yAxisElement => {
        const yAxisD3 = d3Select(yAxisElement);
        const theseUnits = yAxisElement.__data__;
        const thisYScale = scales.y[theseUnits || Scales.unknownUnits];
        // Only show the horizontal grid lines for the main y-axis, or it gets too confusing.
        const tickSizeInner = (thisYScale === mainYScale && defined(state.grid) && state.grid.y) ? -size.width : 3;
        let yTickValues;
        if (state.mini) {
            yTickValues = thisYScale.domain();
        } else {
            const numYTicks = state.mini ? 2 : Math.min(6, Math.floor(size.plotHeight / 30) + 1);
            yTickValues = Scales.truncatedTickValues(thisYScale, numYTicks);
        }
        yAxis
            .tickValues(yTickValues)
            .tickSizeInner(tickSizeInner)
            .scale(thisYScale);
        yAxisD3
            .transition(t)
            .style('opacity', hasData ? 1 : 1e-6)
            .select('.ticks')
                .call(yAxis);
        let colorKeyHtml = '';
        if (allUnits.length > 1) {
            const unitColors = data.filter(line=>(line.units === theseUnits)).map(line=>line.color);
            unitColors.forEach((color, index)=>{
                const y = -1 - index * 4;
                colorKeyHtml += '<path d="M-30 ' + y + ' h 30" style="stroke:' + color + '"/>';
            });
        }
        yAxisD3.select('.colors').html(colorKeyHtml);
        yAxisD3.select('.units-label').text(theseUnits || '');
        yAxisD3.select('.units-label-shadow').text(theseUnits || '');
    });
}
Esempio n. 9
0
  static render(container, state) {
    if (!defined(state.data)) {
      return;
    }

    initializeChartTypes(state);

    const data = state.data;
    const margin = defaultValue(state.margin, defaultMargin);
    const units = uniq(
      state.data.filter(data => data.type !== "moment").map(data => data.units)
    );
    const size = Size.calculate(
      container,
      margin,
      state,
      state.mini ? 1 : units.length
    );
    const scales = Scales.calculate(
      size,
      state.domain,
      state.data,
      this.getXpadding(state)
    );

    const transitionDuration = defaultValue(
      state.transitionDuration,
      defaultTransitionDuration
    );

    // The last condition in hasData checks that at least one y-value of one chart is defined.
    const hasData =
      data.length > 0 &&
      data[0].points.length > 0 &&
      data.some(d => d.points.some(p => defined(p.y)));

    const d3Container = d3Select(container);

    // Update SVG
    const chartSVGContainer = d3Container
      .select("svg")
      .attr("width", state.width)
      .attr("height", state.height)
      .style("user-select", "none")
      .style("cursor", "default");

    // Update Plot Area
    const plotArea = d3Container.select("rect.plot-area");
    plotArea.attr("width", size.width).attr("height", size.plotHeight);

    // Update chart area
    size.yChartOffset = margin.top + Title.getHeight(state.titleSettings);
    const yOffsets = computeYOffset(state, units, scales, size);

    // Adjust x offset
    let totalOffset;
    if (yOffsets.length > 0) {
      totalOffset = yOffsets.reduce((a, b) => a + b);
    } else {
      const remainingGap = container.clientWidth - size.width;
      totalOffset = Math.min(Size.yAxisWidth, remainingGap / 2);
    }
    size.yAxesWidth = totalOffset;

    const chartTransform =
      "translate(" +
      (margin.left + size.yAxesWidth) +
      "," +
      size.yChartOffset +
      ")";
    const chartTransition = d3Transition().duration(transitionDuration);

    const chart = d3Select(container)
      .selectAll(".base-chart")
      .attr("transform", chartTransform);
    const chartPlotContainer = chart.select(".content");

    Title.enterUpdateAndExit(
      d3Container,
      state.titleSettings,
      margin,
      state,
      transitionDuration
    );

    const renderContext = {
      // helpful baseline input
      container,
      state,

      // helpful constraints
      size,
      yOffsets,
      margin,
      scales,
      units,

      // helpful chart specifics
      chart,
      chartPlotContainer,
      chartTransform,
      chartTransition,
      chartSVGContainer
    };
    // Axes.
    if (defined(scales)) {
      this.renderAxis(renderContext, hasData);
    }

    this.renderChart(renderContext);

    this.renderToolTip(renderContext, hasData);

    this.renderHighlight(renderContext);

    // No data. Show message if no data to show.
    this.renderNoData(renderContext, hasData);
  }
Esempio n. 10
0
export default function tip(id) {
  let d3_tip_functor = v => (typeof v === "function" ? v : () => v);
  let d3_tip_direction = () => 'n';
  let d3_tip_offset = () => [0, 0];
  let d3_tip_html = () => ' ';
  let IsDOMElement = o => o instanceof Node;


  dummy(); // dummy injection for transition
  
  let direction = d3_tip_direction,
      offset    = d3_tip_offset,
      html      = d3_tip_html,
      classed   = 'd3-tip',
      node      = initNode(),
      point     = null,
      target    = null,
      parent    = null,
      theme     = 'light',
      transition= false,
      style     = undefined,
      zIndex    = 16777271;

  function initNode() {
    let node = select('div' + (id ?  '#' + id : '.' + classed));
    if (node.empty())
      node = select(document.createElement('div'))
    node
      .attr('id', id)
      .attr('class', classed)
      .style('position','absolute')
      .style('top', 0)
      .style('left', 0)
      .style('opacity', 0)
      .style('pointer-events', 'none')
      .style('box-sizing', 'border-box');
    return node.node()
  }

  function getSVGNode(el) {
    el = el.node()
    if(!el) return;
    return el.tagName.toLowerCase() === 'svg' ? el : el.ownerSVGElement;
  }

  function getNodeEl() {
    //TODO: this check might not be valid any more  
    if(node === null) {
      node = initNode();
      // re-add node to DOM
      parent.appendChild(node);
    }
    return select(node);
  }

  function _impl(vis) {
    if(!parent) {
      document.body.appendChild(node);
    }
    let svg = getSVGNode(vis)
    if (!svg) return;
    
    if (svg.createSVGPoint != null) {
      point = svg.createSVGPoint();
    }
    svg = select(svg);

    let defsEl = svg.select('defs');
    if (defsEl.empty()) {
      defsEl = svg.append('defs');
    }
    
    let _style = style;
    if (_style === undefined) {
      _style = _impl.defaultStyle(theme, DEFAULT_WIDTH);
    }

    let styleEl = defsEl.selectAll('style' + (id ?  '#style-tip-' + id : '.style-' + classed)).data(_style ? [ _style ] : []);
    styleEl.exit().remove();
    styleEl = styleEl.enter()
                  .append('style')
                    .attr('type', 'text/css')
                    .attr('id', (id ?  'style-tip-' + id : null))
                    .attr('class', (id ?  null : 'style-' + classed))
                  .merge(styleEl);
    styleEl.text(s => s);
  }

  _impl.self = function() { return 'div' + (id ?  '#' + id : '.' + classed); }

  _impl.id = function() { return id; };
    
  _impl.classed = function(_) {
    return arguments.length ? (classed = _, _impl) : classed;
  };

  // Public - show the tooltip on the screen
  //
  // Returns a tip
  _impl.show = function() {
    if(!parent) _impl.parent(document.body);
    let args = [].slice.call(arguments);
    target = this;
    let standalone = false;
    if(args.length === 1 && IsDOMElement(args[0])){
      target = args[0];
      args[0] = target.__data__;
      standalone = true;
    }

    let content = html.apply(target, args);
    if (content === null) return _impl;
    
    let poffset = offset.apply(target, args),
        dir     = direction ? direction.apply(target, args) : null,
        nodel   = getNodeEl(),
        i       = directions.length,
        parentCoords = node.offsetParent.getBoundingClientRect();

    while(i--) nodel.classed(directions[i], false);

    if (content != null) {
      nodel.html(content)
    }
    
    if (dir) {
      nodel.classed(dir, true);
    } else {
      dir = 'n';
    }

    let coords = direction_callbacks[dir].apply(target);

    nodel
      .style('top', (coords.top +  poffset[0]) - parentCoords.top + 'px')
      .style('left', (coords.left + poffset[1]) - parentCoords.left + 'px')
      .style('z-index', zIndex);

    if(standalone){
      window.addEventListener('load', function() {
        // for testing
        // console.log('offsets',node.offsetHeight, node.offsetWidth)
        coords = direction_callbacks[dir].apply(target);
        nodel
            .style('top', (coords.top +  poffset[0]) - parentCoords.top + 'px')
            .style('left', (coords.left + poffset[1]) - parentCoords.left + 'px')
      });
    }

    if (transition != null && transition !== false) {
      nodel = nodel.transition();
      if (typeof transition === 'number') {
        nodel = nodel.duration(transition);
      }
    }

    nodel.style('opacity', 1.0);

    return _impl;
  }

  // Public - hide the tooltip
  //
  // Returns a tip
  _impl.hide = function() {
    let nodel = getNodeEl();
    
    if (nodel.interrupt) {
      nodel.interrupt(); // stop the fade in if happening
    }
    nodel.style('opacity', 0.0);
    return _impl;
  }

  // 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
  _impl.attr = function(n) {
    if (arguments.length < 2 && typeof n === 'string') {
      return getNodeEl().attr(n)
    } else {
      let args =  [].slice.call(arguments)
      selection.prototype.attr.apply(getNodeEl(), args)
    }

    return _impl;
  }

  // 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
  _impl.direction = function(v) {
    if (!arguments.length) return direction
    direction = v == null ? v : d3_tip_functor(v)

    return _impl;
  }

  // Public: Sets or gets the offset of the tip
  //
  // v - Array of [x, y] offset
  //
  // Returns offset or
  _impl.offset = function(v) {
    if (!arguments.length) return offset
    offset = v == null ? v : d3_tip_functor(v)

    return _impl;
  }

  // Public: sets or gets the html value of the tooltip
  //
  // v - String value of the tip
  //
  // Returns html value or tip
  _impl.html = function(v) {
    if (!arguments.length) return html
    html = v == null ? v : d3_tip_functor(v)

    return _impl;
  }

  // Public: destroys the tooltip and removes it from the DOM
  //
  // Returns a tip
  _impl.destroy = function() {
    if(node) {
      getNodeEl().remove();
      node = null;
    }
    return _impl;
  }

  _impl.style = function(_) {
    return arguments.length ? (style = _, _impl) : style;
  }
  
  _impl.transition = function(_) {
    return arguments.length ? (transition = _, _impl) : transition;
  }

  _impl.theme = function(_) {
    return arguments.length ? (theme = _, _impl) : theme;
  }  
  
  _impl.zIndex = function(_) {
    return arguments.length ? (zIndex = _, _impl) : zIndex;
  } 

  _impl.parent = function(v) {
    if (!arguments.length) return parent;
    parent = v || document.body;
    parent.appendChild(node);

    // Make sure offsetParent has a position so the tip can be
    // based from it. Mainly a concern with <body>.
    let offsetParent = select(node.offsetParent)
    if (offsetParent.style('position') === 'static') {
     offsetParent.style('position', 'relative')
    }

    return _impl;
  }

  _impl.defaultStyle = (_theme, _width) => `
                  ${fonts.fixed.cssImport}  
                  ${_impl.self()} {
                                    line-height: 1;
                                    font-family: ${fonts.fixed.family};
                                    color: ${display[_theme].negative.text};
                                    font-weight: ${fonts.fixed.weightMonochrome};  
                                    font-size: ${fonts.fixed.sizeForWidth(_width)};  
                                    padding: 8px;
                                    background: ${display[_theme].negative.background};
                                    border-radius: 2px;
                                    pointer-events: none;
                                  }
                    /* Creates a small triangle extender for the tooltip */
                    ${_impl.self()}:after {
                                      box-sizing: border-box;
                                      display: inline;
                                      width: 100%;
                                      line-height: 1;
                                      color: ${display[_theme].negative.background};
                                      font-size: ${fonts.fixed.sizeForWidth(1)};  
                                      position: absolute;
                                      pointer-events: none;
                                    }
                    /* Northward tooltips */
                    ${_impl.self()}.n:after {
                                      content: "\\25bc";
                                      margin: -3px 0 0 0;
                                      top: 100%;
                                      left: 0;
                                      text-align: center;
                                    }
                    /* Eastward tooltips */
                    ${_impl.self()}.e:after {
                                      content: "\\25C0";
                                      margin: -7px 0 0 0;
                                      top: 50%;
                                      left: -7px;
                                    }
                    /* Southward tooltips */
                    ${_impl.self()}.s:after {
                                      content: "\\25B2";
                                      margin: 0 0 1px 0;
                                      top: -10px;
                                      left: 0;
                                      text-align: center;
                                    }
                    /* Westward tooltips */
                    ${_impl.self()}.w:after {
                                      content: "\\25B6";
                                      margin: -7px 0 0 0;
                                      top: 50%;
                                      left: 100%;
                                    }                
                `;

  function direction_n() {
    let bbox = getScreenBBox()
    return {
      top:  (bbox.n.y - node.offsetHeight),
      left: (bbox.n.x - node.offsetWidth / 2)
    }
  }

  function direction_s() {
    let bbox = getScreenBBox()
    return {
      top:  (bbox.s.y),
      left: (bbox.s.x - node.offsetWidth / 2)
    }
  }

  function direction_e() {
    let bbox = getScreenBBox()
    return {
      top:  (bbox.e.y - node.offsetHeight / 2),
      left: (bbox.e.x)
    }
  }

  function direction_w() {
    let bbox = getScreenBBox()
    return {
      top:  (bbox.w.y - node.offsetHeight / 2),
      left: (bbox.w.x - node.offsetWidth)
    }
  }

  function direction_nw() {
    let bbox = getScreenBBox()
    return {
      top:  (bbox.nw.y - node.offsetHeight),
      left: (bbox.nw.x - node.offsetWidth)
    }
  }

  function direction_ne() {
    let bbox = getScreenBBox()
    return {
      top:  (bbox.ne.y - node.offsetHeight),
      left: (bbox.ne.x)
    }
  }

  function direction_sw() {
    let bbox = getScreenBBox()
    return {
      top:  (bbox.sw.y),
      left: (bbox.sw.x - node.offsetWidth)
    }
  }

  function direction_se() {
    let bbox = getScreenBBox()
    return {
      top:  (bbox.se.y),
      left: (bbox.se.x)
    }
  }

  // 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() {
    let targetel   = target || event.target;

    while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
        targetel = targetel.parentNode;
    }

    let bbox       = {},
        matrix     = targetel.getScreenCTM(),
        tbbox      = targetel.getBBox(),
        width      = tbbox.width,
        height     = tbbox.height,
        x          = tbbox.x,
        y          = tbbox.y

    point.x = x
    point.y = y
    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;
  }

  let direction_callbacks = {
    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 = Object.keys(direction_callbacks);

  return _impl;
}