Improvements to D3’s Reusable Component Pattern

Updated: 12/26/2012

If you’re new to D3, it’s a dynamic, DOM-element-creating-and-data-binding, SVG-path-,-style-,-and-transform-specifying, swiss army knife for data visualization that is –as of v3– capable of generating canvas path data!

Take a look at the range of stuff developed with it over here:

https://github.com/mbostock/d3/wiki/Gallery

It was suggested a while ago, as a starting point, to use the following pattern for building data visualization components that mimic D3’s own components in terms of configuration and use patterns. It was called the “Reusable chart Pattern” and the gist of it is this:


function chart() {
  var width = 720, // default width
      height = 80; // default height

  function my() {
    // generate chart here, using `width` and `height`
  }

  my.width = function(value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  };

  my.height = function(value) {
    if (!arguments.length) return height;
    height = value;
    return my;
  };

  return my;
}

Source: http://bost.ocks.org/mike/chart/

I have had a few key requirements on several occasions that are not provided by the original pattern above:

1. Getter/Setter methods needs to be generated automatically (why: because there may be many of them)

2. In the component code itself, properties that will be exposed publicly need to be defined separately than local vars (this is needed both for clarity/abstraction and for achieving #1 above)

3. Must be able to assign functions to properties by reference or value, not just from inside the component but also from outside of it, where in both cases “this” in the function body must refer to the component instance.

So here is what I came up with:


	// this function (when called from inside a component) creates automatic getter/setter on the returned object 
	// for each public property so that public properties can be accessed as componentInstance.property(value) 
	// (when setting) and componentInstance.property() (when getting)
    function getterSetter()  {
      for (o in this.props) {
           if (this.props.hasOwnProperty(o)) {         
                this[o] =  new Function("value", "if (!arguments.length) return typeof this.props['" + o + 
                "'] == 'function' ? this.props['" + o + "'].call(this) : this.props['" + o + "'];" + 
                "this.props['" + o + "'] = (typeof value == 'function' ? value.call(this) : value); return this") 
            }
       }
    }
    
    // create component 
	var myChart = function chart(args) {
		
		// private vars defined ordinarily 
		var a, b, c, d, etc;
		  
		// let's refer to the instance as obj	
		var obj = {};
		  
		// place to hold publicly accessible properties	
		obj.props = {};
		  
		// all publicly accessible properties are defined here
		obj.props.width=60;
		obj.props.height=70;
		  
		obj.props.area = function() {	  	
			return this.props.height *  this.props.width 
		}
		
		obj.props.someArray = [];
		
		obj.props.test = function() {
			return Math.random()
		}
		
		// create getters/setters 
		getterSetter();
		
		// this is how private methods are defined
		var somePrivateMethod = function() { console.log('hi, i am a private method')}
		  
		// this is how public methods are defined... 
		obj.render = function() {
	  	
			// generate chart here, using `obj.props.width` and `obj.props.height`
			    
			console.log('rendering chart with width = ' + obj.props.width +  ' and height = ' + obj.props.height)
			    
			if (args) console.log('detected component scoped argument: ' + args)
			    
			return obj;
		}
	
		return obj; 
	}
	
    // testing the original settings
	myChart().render()
	console.log(myChart().width(), myChart().height())

	// create new instances with new settings
	var chart1 = myChart()
			.width(500)
			.height(300)
			.render()
			
	console.log(chart1.test())		
	console.log(chart1.test())	
	console.log(chart1.test())	

	var chart2 =  myChart()
			.width(200)
			.height(800);
	// example of rendering after setting
	chart2.render()
	
	var chart3 = myChart()
			.width(100)
			// example of function as value generator
			.height(function() {return this.width() * 4})	
			.render()
	var chart4 = myChart(999)
			.width(100)
			.height(120)
			.render()
	
	// testing the new settings
	console.log(chart1.width(), chart1.height())
	console.log(chart2.width(), chart2.height())
	console.log(chart3.width(), chart3.height())
        
	// set a new width for chart3
	chart3.width(3000)
	
	console.log(chart3.width(), chart3.height())
	
	console.log(chart4.width(), chart4.height())
	
	// tests for function reference as value for setter
	
	// test getter for property
	console.log('area property with default function', chart1.area())
	
	// set property to function reference via closure (use only a function reference if assigning by value)
	// else use two as shown 
	chart1.area(function() {return function() { return Math.pow(this.width(), 2) * (Math.PI/4)}})
	
	// test getter after change
	console.log('area property for chart1 with updated function', chart1.area())
	
	console.log('area property for chart1 with updated function after width is changed', chart1.width(700).area())
	
	console.log('area property for chart2 with original function', chart2.area())
	
        // test assigning a closure to a property
	function testClosure() {	
		var someFunction = function() {
			return ~~(Math.random() * 1000);
		}
		return function() {
			return someFunction();
		}
	}
	
    chart1.width(testClosure)
	
    console.log(chart1.width())
	console.log(chart1.width())
	console.log(chart1.width())
	
	// using typed properties
	chart1.someArray().push(5) // or chart1.someArray([1,2,3])
	
	console.log(chart1.someArray())
	
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s