Harry Stevens
Harry Stevens

Reputation: 1423

Chaining functions in a D3.js plugin

I have a written a plugin for D3.js called d3-marcon that implements Mike Bostock's margin conventions. For example, rather than having to write:

var margin = {top: 10, bottom: 10, left: 10, right: 10},
    width = window.innerWidth - margin.left - margin.right,
    height = window.innerHeight - margin.top - margin.bottom,
    svg = d3.select("body").append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom)
        .append("g")
            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

You can write:

var setup = d3.marcon({top: 10, bottom: 10, left: 10, right: 10, width: window.innerWidth, height: window.innerHeight}),
    margin = setup.margin,
    width = setup.width,
    height = setup.height,
    svg = setup.svg;

As you can see, it works by passing an options object to the marcon() function. If you specify no options, it defaults to 0 for all margins, 900 for the width, 600 for the height, and appends the svg element to the "body". So you can get up and running very quickly with just one line of code, var setup = d3.marcon(), and then pass options later when you want to change your viz.

This is useful, but it still doesn't feel like a real D3 function. Real D3 functions allow for chaining functions together, rather than passing options objects. So instead of d3.marcon({element: ".viz"}), D3 code looks like d3.marcon().element(".viz").

D3 code also lets you pass additional functions to the chained functions (e.g. d3.method().chainedFunction(function(d) { return d.value; })), so you can update the attributes of an object based on data.

Obviously, my plugin does none of these things. I've spent a few hours looking at existing D3 modules to try to figure out how they work, but I'm not getting anywhere. Can anyone suggest how to get my code to work like a proper D3 module? Or, failing that, a good tutorial to read?

I linked to the repository above, but here it is again. And here is a block showing how it works.

Upvotes: 3

Views: 446

Answers (2)

Gerardo Furtado
Gerardo Furtado

Reputation: 102219

This is my suggestion, based on Nick Zhu's approach in his book Data Visualization with D3 4.x.

Basically, we create a function with an object...

function marcon() {
    var instance = {};
}

... and set each method individually:

instance.top = function(d) {
    if (!arguments.length) return top;
    top = d;
    return instance;
};

At the end, you call the render part with render():

marcon().top(10)
    .left(10)
    //etc...
    .render();

The nice thing about this approach is that, as you requested, it allows chaining. For instance, you can create your SVG like this:

var mySvg = marcon();

mySvg.top(20)
    .left(10)
    .right(10)
    .bottom(10)
    .height(200)
    .width(200)
    .element("body")
    .render();

Here is a demo:

function marcon() {
  var instance = {};
  var top = 10,
    bottom = 0,
    left = 0,
    right = 0,
    width = 900,
    height = 600,
    element = "body",
    innerWidth, innerHeight, svg;

  instance.top = function(d) {
    if (!arguments.length) return top;
    top = d;
    return instance;
  };

  instance.left = function(d) {
    if (!arguments.length) return left;
    left = d;
    return instance;
  };

  instance.right = function(d) {
    if (!arguments.length) return right;
    right = d;
    return instance;
  };

  instance.bottom = function(d) {
    if (!arguments.length) return bottom;
    bottom = d;
    return instance;
  };

  instance.width = function(d) {
    if (!arguments.length) return width;
    width = d;
    return instance;
  };

  instance.height = function(d) {
    if (!arguments.length) return height;
    height = d;
    return instance;
  };

  instance.element = function(d) {
    if (!arguments.length) return element;
    element = d;
    return instance;
  };

  instance.innerWidth = function() {
    return innerWidth;
  };

  instance.innerHeight = function() {
    return innerHeight;
  };

  instance.svg = function() {
    return svg;
  };

  instance.render = function() {
    innerWidth = width - left - right;
    innerHeight = height - top - bottom;
    svg = d3.select(element)
      .append("svg")
      .attr("width", innerWidth + left + right)
      .attr("height", innerHeight + top + bottom)
      .append("g")
      .attr("transform", "translate(" + left + ", " + top + ")");
  }

  return instance;
}

var mySvg = marcon();
mySvg.top(20)
  .left(10)
  .right(10)
  .bottom(20)
  .height(200)
  .width(200)
  .element(".testDiv")
  .render();

var rect = mySvg.svg()
  .append("rect")
  .attr("width", mySvg.innerWidth())
  .attr("height", mySvg.innerHeight())
  .style("fill", "teal")
svg {
  background-color: tan;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div class="testDiv"></div>

Also, it allows defaults. If you don't set a setter, it defaults to the specified values. Here, if we don't set the width, it defaults to 900:

function marcon() {
  var instance = {};
  var top = 10,
    bottom = 0,
    left = 0,
    right = 0,
    width = 900,
    height = 600,
    element = "body",
    innerWidth, innerHeight, svg;

  instance.top = function(d) {
    if (!arguments.length) return top;
    top = d;
    return instance;
  };

  instance.left = function(d) {
    if (!arguments.length) return left;
    left = d;
    return instance;
  };

  instance.right = function(d) {
    if (!arguments.length) return right;
    right = d;
    return instance;
  };

  instance.bottom = function(d) {
    if (!arguments.length) return bottom;
    bottom = d;
    return instance;
  };

  instance.width = function(d) {
    if (!arguments.length) return width;
    width = d;
    return instance;
  };

  instance.height = function(d) {
    if (!arguments.length) return height;
    height = d;
    return instance;
  };

  instance.element = function(d) {
    if (!arguments.length) return element;
    element = d;
    return instance;
  };

  instance.innerWidth = function() {
    return innerWidth;
  };

  instance.innerHeight = function() {
    return innerHeight;
  };

  instance.svg = function() {
    return svg;
  };

  instance.render = function() {
    innerWidth = width - left - right;
    innerHeight = height - top - bottom;
    svg = d3.select(element)
      .append("svg")
      .attr("width", innerWidth + left + right)
      .attr("height", innerHeight + top + bottom)
      .append("g")
      .attr("transform", "translate(" + left + ", " + top + ")");
  }

  return instance;
}

var mySvg = marcon();
mySvg.top(20)
  .left(10)
  .right(10)
  .bottom(20)
  .height(200)
  .element("body")
  .render();

var rect = mySvg.svg()
  .append("rect")
  .attr("width", mySvg.innerWidth())
  .attr("height", mySvg.innerHeight())
  .style("fill", "teal")
svg {
  background-color: tan;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div class="testDiv"></div>

Finally, you can use getters, like:

marcon().top();

Which gives you the value. Here is a demo, have a look at the console:

function marcon() {
  var instance = {};
  var top = 10,
    bottom = 0,
    left = 0,
    right = 0,
    width = 900,
    height = 600,
    element = "body",
    innerWidth, innerHeight, svg;

  instance.top = function(d) {
    if (!arguments.length) return top;
    top = d;
    return instance;
  };

  instance.left = function(d) {
    if (!arguments.length) return left;
    left = d;
    return instance;
  };

  instance.right = function(d) {
    if (!arguments.length) return right;
    right = d;
    return instance;
  };

  instance.bottom = function(d) {
    if (!arguments.length) return bottom;
    bottom = d;
    return instance;
  };

  instance.width = function(d) {
    if (!arguments.length) return width;
    width = d;
    return instance;
  };

  instance.height = function(d) {
    if (!arguments.length) return height;
    height = d;
    return instance;
  };

  instance.element = function(d) {
    if (!arguments.length) return element;
    element = d;
    return instance;
  };

  instance.innerWidth = function() {
    return innerWidth;
  };

  instance.innerHeight = function() {
    return innerHeight;
  };

  instance.svg = function() {
    return svg;
  };

  instance.render = function() {
    innerWidth = width - left - right;
    innerHeight = height - top - bottom;
    svg = d3.select(element)
      .append("svg")
      .attr("width", innerWidth + left + right)
      .attr("height", innerHeight + top + bottom)
      .append("g")
      .attr("transform", "translate(" + left + ", " + top + ")");
  }

  return instance;
}

var mySvg = marcon();
mySvg.top(20)
  .left(10)
  .right(10)
  .bottom(20)
  .height(200)
  .width(200)
  .element("body")
  .render();

var rect = mySvg.svg()
  .append("rect")
  .attr("width", mySvg.innerWidth())
  .attr("height", mySvg.innerHeight())
  .style("fill", "teal");
  
console.log("The height is " + mySvg.height())
svg {
  background-color: tan;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div class="testDiv"></div>

Upvotes: 3

Mark
Mark

Reputation: 108567

There's a couple different ways to do what you are after. First, you should read this excellent tutorial on re-usability with d3. Taking those ideas with your code would look something like this:

<!DOCTYPE html>
<html>

<head>
  <script data-require="[email protected]" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>

<body>
  <script>
    function marcon() {
      
      var top = 0,
      	bottom = 0,
      	left = 0,
      	right = 0,
      	width = 900,
      	height = 600,
      	svg;

      function self(selection) {
        
    		var w = width - left - right,
    		    h = height - top - bottom;
    		
    		svg = selection.append("svg")
    		  .attr("width", width + left + right)
    			.attr("height", height + top + bottom)
    			.append("g")
    			.attr("transform", "translate(" + left + ", " + top + ")");
    	}

      self.top = function(value) {
        if (!arguments.length) return top;
        top = value;
        return self;
      };

      self.bottom = function(value) {
        if (!arguments.length) return bottom;
        bottom = value;
        return self;
      };
      
      self.left = function(value) {
        if (!arguments.length) return left;
        left = value;
        return self;
      };
      
      self.right = function(value) {
        if (!arguments.length) return right;
        right = value;
        return self;
      };
      
      self.width = function(value) {
        if (!arguments.length) return width;
        width = value;
        return self;
      };
      
      self.height = function(value) {
        if (!arguments.length) return height;
        height = value;
        return self;
      };
      
      self.svg = function(value){
        if (!arguments.length) return svg;
        svg = value;
        return self;
      }


      return self;
    }
    
    var m = marcon()
      .width(100)
      .height(100)
      .top(50)
      .left(50);
    
    d3.select('body')
      .call(m);
      
    m.svg()
      .append("text")
      .text("Hi Mom!");
    
  </script>
</body>

</html>

Upvotes: 2

Related Questions