block by micahstubbs f94ee71c353f0152da3bc2ee4351608d

d3 v3 --> v4 upgrade mystery

Full Screen

a work-in-progress d3 v4 fork of the bl.ock reusable updating exploding boxplot from @tennisvisuals

currently stuck in this strange limbo state where there are no errors in the Chrome devtools console, yet there is no visualization rendered on-screen.

I suspect that the missing piece has something to do with one of these three d3 API changes:

click the Default Colors radio button, then open up the console to see the strange lack of errors for yourself 😅

If you figure out how to render vis with d3 v4 do tweet what you find at @micahstubbs 😄


Original README.md


reusable updating exploding boxplot

Design based on original: mcaule

See this block in production with Live Data

Features:

var container = d3.select('body');
var xbp = explodingBoxplot();
xbp.options({
  data: {
    group: 'Set Score',
    color_index: 'Set Score',
    identifier: 'h2h'
  },
  axes: {
    x: { label: 'Set Score' },
    y: { label: 'Total Points' }
  }
});
xbp.data(data);
container.call(xbp);
xbp.update();

Change the dimension for y axis:

xbp.options( { axes: { y: { label: 'Total Shots' } } });
xbp.update();

Data for this example was generated by mcpParse

Accessors:

by default accessors with no parameters return current values

operations on data held in chart instance

index.html

<!DOCTYPE html>
<html lang='en'>
   <head>
      <meta charset='UTF-8'>
      <meta http-equiv='Content-Type' content='text/html;charset=utf-8'>
      <meta name='Keywords' content='Tennis, Set Scores, ATP World Tour, interactive, TennisVisuals, sport, infographic, graphic, data visualisation'>
      <meta name='viewport' content='width=device-width'>
      <title>Point Distributions by Set Scores</title>
      <meta name='twitter:creator' content='@TennisVisuals'>
      <meta name='twitter:url' content='//TennisVisuals.com/Distributions'/>
      <meta name='twitter:title' content='Point Distributions by Set Score'>
      <meta name='twitter:description' content='Compare Tennis Set Scores by Number of Points and Number of Shots Played'>
      <meta name='twitter:image' content=''>
      <meta name='og:url' content='//TennisVisuals.com/Distributions'/>

      <script src='https://d3js.org/d3.v4.js'></script>
      <script src='https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js'></script>
      <script src='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js'></script>
      <script src='d3-tip.js'></script>
      <script src='https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js'></script>

      <link href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css' rel='stylesheet'>
      <link rel='icon' href='data:;base64,iVBORw0KGgo='>

      <script src='pDxbp.js' lang='babel' type='text/babel'></script>
      <script src='explodingBoxplot.js' lang='babel' type='text/babel'></script>
      <link rel='stylesheet' type='text/css' href='./explodingBoxplot.css'>
   </head>

   <body id='body'>
      <div id='container' class='container-fluid text-center'>
         <h5 id='title' style='color: #3B3B3B;'></h5>
         <div class='col-sm-9'>
            <div id='pointDistributions'></div>
         </div>
         <div class='col-sm-3'>
            <div id='controls'>
            </div>
         </div>
      </div>

      <script lang='babel' type='text/babel'>
         // boxPlotFunctions.defaultDistribution('popover');
         boxPlotFunctions.defaultDistribution('d3-tip');
         boxPlotFunctions.demoSetup();
      </script>
   </body>

</html>

d3-tip.js

// d3.tip
// Copyright (c) 2013 Justin Palmer
// ES6 / D3 v4 Adaption Copyright (c) 2016 Constantin Gavrilete
// Removal of ES6 for D3 v4 Adaption Copyright (c) 2016 David Gotz
//
// Tooltips for d3.js SVG visualizations

d3.functor = function functor(v) {
  return typeof v === "function" ? v : function() {
    return v;
  };
};

d3.tip = 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   = getNodeEl(),
        i       = directions.length,
        coords,
        scrollTop  = document.documentElement.scrollTop || document.body.scrollTop,
        scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft

    nodel.html(content)
      .style('position', 'absolute')
      .style('opacity', 1)
      // comment this out to avoid flickering tooltips
      // .style('pointer-events', 'all')

    while(i--) nodel.classed(directions[i], false)
    coords = direction_callbacks[dir].apply(this)
    nodel.classed(dir, true)
      .style('top', (coords.top +  poffset[0]) + scrollTop + 'px')
      .style('left', (coords.left + poffset[1]) + scrollLeft + 'px')

    return tip
  }

  // Public - hide the tooltip
  //
  // Returns a tip
  tip.hide = function() {
    var nodel = getNodeEl()
    nodel
      .style('opacity', 0)
      .style('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 getNodeEl().attr(n)
    } else {
      var args =  Array.prototype.slice.call(arguments)
      d3.selection.prototype.attr.apply(getNodeEl(), 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) {
    // debugger;
    if (arguments.length < 2 && typeof n === 'string') {
      return getNodeEl().style(n)
    } else {
      var args = Array.prototype.slice.call(arguments);
      if (args.length === 1) {
        var styles = args[0];
        Object.keys(styles).forEach(function(key) {
          return d3.selection.prototype.style.apply(getNodeEl(), [key, styles[key]]);
        });
      }
    }

    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
  }

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

  function d3_tip_direction() { return 'n' }
  function d3_tip_offset() { return [0, 0] }
  function d3_tip_html() { return ' ' }

  var 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
  };

  var directions = Object.keys(direction_callbacks);

  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')
      .style('top', 0)
      .style('opacity', 0)
      .style('pointer-events', 'none')
      .style('box-sizing', 'border-box')

    return node.node()
  }

  function getSVGNode(el) {
    el = el.node()
    if(el.tagName.toLowerCase() === 'svg')
      return el

    return el.ownerSVGElement
  }

  function getNodeEl() {
    if(node === null) {
      node = initNode();
      // re-add node to DOM
      document.body.appendChild(node);
    };
    return d3.select(node);
  }

  // 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;

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

    var 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
  }

  return tip
};

explodingBoxplot.css

#container { position: relative; width: 1000px; padding: 10px }

.axis {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

line.explodingBoxplot.line,
rect.explodingBoxplot.box
{
  stroke: #888;
  stroke-width: 2px;
}

line.explodingBoxplot.vline{
  stroke-dasharray:5,5;
}

.explodingBoxplot.tip{
  font: normal 13px 'Lato', 'Open sans', sans-serif;
  line-height: 1;
  font-weight: bold;
  padding: 12px;
  background: #333333;
  color: #DDDDDD;
  border-radius: 2px;
}

g.tick text,
g.axis text{
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -o-user-select: none;
  user-select: none;
  cursor: default;
}

pDxbp.js

/*
eslint
no-undef: "off",
func-names: "off",
no-use-before-define: "off",
no-console: "off",
no-unused-vars: "off",
no-unused-expressions: "off"
*/
const boxPlotFunctions = {};

boxPlotFunctions.removeTooltip = removeTooltip;
function removeTooltip(d, i, element) {
  if (!$(element).popover) return;
  $('.popover').each(function () {
    $(this).remove();
  });
}

boxPlotFunctions.showTooltip = showTooltip;
function showTooltip(d, i, element, constituents, options) {
  if (!$(element).popover) return;
  $(element).popover({
    placement: 'auto top',
    container: `#${constituents.elements.domParent.attr('id')}`,
    trigger: 'manual',
    html: true,
    content() {
      const identifier = options.data.identifier && d[options.data.identifier] ?
             d[options.data.identifier] : 'undefined';

      const value = options.axes.y.label && d[options.axes.y.label] ?
             options.axes.y.tickFormat(d[options.axes.y.label]) : '';

      let message = "<span style='font-size: 11px; text-align: center;'>";
      message += `${d[options.data.identifier]}: ${d[options.axes.y.label]}</span>`;

      return message;
    }
  });
  $(element).popover('show');
}

boxPlotFunctions.defineTooltip = defineTooltip;
function defineTooltip(constituents, options, events) {
  const tip = d3.tip().attr('class', 'explodingBoxplot tip')
          .direction('n')
          .html(tipFunction);

  function tipFunction(d) {
    const color = options.data.color_index && d[options.data.color_index] ?
           constituents.scales.color(d[options.data.color_index]) : 'blue';

    const identifier = options.data.identifier && d[options.data.identifier] ?
           d[options.data.identifier] : 'undefined';

    const value = options.axes.y.label && d[options.axes.y.label] ?
           options.axes.y.tickFormat(d[options.axes.y.label]) : '';

    const message = `<span style="color:${color}">${identifier}</span>
      <span style="color:#DDDDDD;" > : ${value}</span>`;
    return message;
  }

  events.point.mouseover = tip.show;
  events.point.mouseout = tip.hide;

  if (constituents.elements.chartRoot) constituents.elements.chartRoot.call(tip);
}

boxPlotFunctions.defaultDistribution = defaultDistribution;
function defaultDistribution(tooltip) {
  const defaultDistributions = 'atpWta.json';
  const container = d3.select('#pointDistributions');

  d3.json(defaultDistributions, (error, result) => {
    if (error || !result) return;

    const xbp = explodingBoxplot();
    boxPlotFunctions.xbp = xbp;

    if (tooltip) {
      if (tooltip === 'popover') {
        xbp.events({
          point: {
            mouseover: showTooltip,
            mouseout: removeTooltip
          }
        });
      }
      if (tooltip === 'd3-tip') {
        xbp.events({
          update: {
            ready: defineTooltip
          }
        });
      }
    }

    xbp.options(
      {
        id: 'demo',
        data: {
          group: 'Set Score',
          color_index: 'Set Score',
          identifier: 'h2h'
        },
        width: 700,
        height: 480,
        axes: {
          x: { label: 'Set Score' },
          y: { label: 'Total Points' }
        }
      }
       );

    xbp.data(result.data);
    container.call(xbp);
    xbp.update();
  });
}

boxPlotFunctions.demoSetup = demoSetup;
function demoSetup() {
  let data;
  let originalWidth;
  let originalHeight;

  const vizcontrol = d3.select('#controls');
  const viztable = vizcontrol.append('table')
    .attr('align', 'center');

  const row1 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row1.append('input')
    .attr('name', 'tooltip')
    .attr('id', 'popover')
    .attr('type', 'radio')
    .attr('value', 'popover');

  row1.append('label')
    .html('&nbsp; Bootstrap Popover')
    .style('font-size', '12px');

  document.getElementById('popover').addEventListener('change', () => {
    boxPlotFunctions.xbp.events({
      point: {
        mouseover: showTooltip,
        mouseout: removeTooltip
      },
      update: { ready: null }
    });
  });

  const row2 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row2.append('input')
    .attr('name', 'tooltip')
    .attr('id', 'd3tip')
    .attr('type', 'radio')
    .attr('value', 'd3tip')
    .attr('checked', 'checked');

  row2.append('label')
    .html('&nbsp; d3-tip Tooltip')
    .style('font-size', '12px');

  document.getElementById('d3tip').addEventListener('change', () => {
    boxPlotFunctions.xbp.events({
      update: { ready: defineTooltip }
    });
    boxPlotFunctions.xbp.update();
  });

  const row3 = viztable.append('tr')
    .append('td')
    .append('hr');

  const row4 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row4.append('input')
    .attr('name', 'colors')
    .attr('id', 'shuffle')
    .attr('type', 'radio')
    .attr('value', 'shuffle');

  row4.append('label')
    .html('&nbsp; Shuffle Colors')
    .style('font-size', '12px');

  document.getElementById('shuffle').addEventListener('change', () => {
    const shuffleColors = {
      7: '#a6cee3',
      4: '#ff7f00',
      1: '#b2df8a',
      3: '#1f78b4',
      2: '#fdbf6f',
      0: '#33a02c',
      5: '#cab2d6',
      8: '#6a3d9a',
      9: '#fb9a99',
      6: '#e31a1c',
      11: '#ffff99',
      10: '#b15928'
    };
    boxPlotFunctions.xbp.colors(shuffleColors);
    boxPlotFunctions.xbp.update();
  });

  const row5 = viztable
    .append('tr')
    .append('td')
    .attr('align', 'left');

  row5.append('input')
    .attr('name', 'colors')
    .attr('id', 'default')
    .attr('type', 'radio')
    .attr('value', 'default')
    .attr('checked', 'checked');

  row5.append('label')
    .html('&nbsp; Default Colors')
    .style('font-size', '12px');

  document.getElementById('default').addEventListener('change', () => {
    boxPlotFunctions.xbp.colors({ foo: 'bogus' });
    boxPlotFunctions.xbp.update();
  });

  const row6 = viztable.append('tr')
    .append('td')
    .append('hr');

  const row7 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row7.append('input')
    .attr('name', 'size')
    .attr('id', 'resize')
    .attr('type', 'radio')
    .attr('value', 'resize');

  row7.append('label')
    .html('&nbsp; Resize')
    .style('font-size', '12px');

  document.getElementById('resize').addEventListener('change', () => {
    originalWidth = boxPlotFunctions.xbp.width();
    originalHeight = boxPlotFunctions.xbp.height();
    boxPlotFunctions.xbp.width(400).height(300);
    boxPlotFunctions.xbp.update();
  });

  const row8 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row8.append('input')
    .attr('name', 'size')
    .attr('id', 'original')
    .attr('type', 'radio')
    .attr('value', 'original')
    .attr('checked', 'checked');

  row8.append('label')
    .html('&nbsp; Original Dimensions')
    .style('font-size', '12px');

  document.getElementById('original').addEventListener('change', () => {
    if (originalWidth && originalHeight) {
      boxPlotFunctions.xbp.width(originalWidth).height(originalHeight);
      boxPlotFunctions.xbp.update();
    }
  });

  const row9 = viztable.append('tr')
    .append('td')
    .append('hr');

  const row10 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row10.append('input')
    .attr('name', 'data')
    .attr('id', 'slice')
    .attr('type', 'radio')
    .attr('value', 'slice');

  row10.append('label')
    .html('&nbsp; Slice Data')
    .style('font-size', '12px');

  document.getElementById('slice').addEventListener('change', () => {
    data = boxPlotFunctions.xbp.data();
    boxPlotFunctions.xbp.data(data.slice(1000, 3000));
    boxPlotFunctions.xbp.update();
  });

  const row11 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row11.append('input')
    .attr('name', 'data')
    .attr('id', 'full')
    .attr('type', 'radio')
    .attr('value', 'full')
    .attr('checked', 'checked');

  row11.append('label')
    .html('&nbsp; Original Data')
    .style('font-size', '12px');

  document.getElementById('full').addEventListener('change', () => {
    if (data) {
      boxPlotFunctions.xbp.data(data);
      boxPlotFunctions.xbp.update();
    }
  });

  let row12 = viztable
    .append('tr')
    .append('td')
    .append('hr');

  let row13 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row13.append('input')
    .attr('name', 'attribute')
    .attr('id', 'shots')
    .attr('type', 'radio')
    .attr('value', 'shots');

  row13.append('label')
    .html('&nbsp; Change Attribute')
    .style('font-size', '12px');

  document.getElementById('shots').addEventListener('change', () => {
    boxPlotFunctions.xbp.options({
      axes: {
        y: {
          label: 'Total Shots'
        }
      }
    });
    boxPlotFunctions.xbp.update();
  });

  const row14 = viztable.append('tr')
    .append('td')
    .attr('align', 'left');

  row14.append('input')
    .attr('name', 'attribute')
    .attr('id', 'points')
    .attr('type', 'radio')
    .attr('value', 'points')
    .attr('checked', 'checked');

  row14.append('label')
    .html('&nbsp; Original Attribute')
    .style('font-size', '12px');

  document.getElementById('points').addEventListener('change', () => {
    boxPlotFunctions.xbp.options({
      axes: {
        y: { label: 'Total Points' }
      }
    });
    boxPlotFunctions.xbp.update();
  });

  row12 = viztable.append('tr')
    .append('td')
    .append('hr');

  row13 = viztable.append('tr')
    .append('td')
    .attr('align', 'left')
    .html('Explode: click on boxes<br/>Reset: double-click background');
}