tomgp
3/3/2016 - 11:16 AM

icon array widthFirst or not?

icon array widthFirst or not?

Icon widthFirst()

An example using my d3 icon array plugin illustrating when you may want to to use widthFirst(true) vs widthFirst(false)

<!DOCTYPE html>
<html>
<head>
	<title>Simple icon array example</title>
	<script src="//d3js.org/d3.v4.0.0-alpha.18.min.js" charset="utf-8"></script>
	<script type="text/javascript" src="d3-iconarray.js"></script>
	<style type="text/css">
	*{
		font-family: sans-serif;
	}

	p.bar-label{
		margin:0px;
	}

	.Conservative{
		fill:#00F;
	}
	.Labour{
		fill:#F00;
	}
	.Liberal{
		fill:#EA0;
	}
	.SNP{
		fill:#DD0;
	}
	.DUP, .Other{
		fill:#AAA;
	}
	p{
		max-width: 600px;
	}
	</style>
</head>
<body>
<p>By default the icon array is built width first. Whilst this has some nice properties like using the full width available, leading to a percieved compactness, it doesn't allow easy comparison between arrays. In this case the Conservative and Labour arrays look about the same ... </p>
<div id="width-first">
	
</div>
<p>A height first construction allows a better approximation of a bar chart (comparisons of length are much easier than comparisons of area so we want to emphasise those differences) and the difference between the front runners becomes more apparent.</p>
<div id="height-first">
	
</div>
<p>Note the smaller values become a little tricker to compare but that the ease of countability (is that a word?) which a icon array provides can help here providing more legible information than an equivalent bar chart would at a similar scale.

</body>


<script type="text/javascript">
	
var results = [
	{party:'Conservative', seats:331 },
	{party:'Labour', seats:232},
	{party:'SNP', seats:56},
	{party:'Liberal Democats', seats:8},
	{party:'DUP', seats:8},
	{party:'Other', seats:15}
];

var gridWidth = 80;
var gridHeight = 5;
var layout = d3_iconarray.layout()
	.width(gridWidth)
	.height(gridHeight);
var width = 600;
var height = 40;
var radius = 2.5;
var margin = { top:radius*2, left:radius*2, bottom:radius*2, right:radius*2 }
var scale = d3.scaleLinear()
				.range([0, (width-(margin.left + margin.right))])
				.domain([0, gridWidth]);


console.log(scale.range())

d3.select('#width-first')
	.selectAll('div.result')
		.data(results)
	.enter()
		.append('div').attr('class','result')
			.call(arrayBars, true);

d3.select('#height-first')
	.selectAll('div.result')
		.data(results)
	.enter()
		.append('div').attr('class','result')
			.call(arrayBars, false);

function arrayBars(parent, widthFirst){
		layout.widthFirst(widthFirst);

		parent.append('p')
			.attr('class','bar-label')
			.html(function(d){ 
				return d.party; 
			});

		parent.append('svg')
			.attr('width', width).attr('height', height)
		.append('g')
			.attr('transform','translate('+margin.left+','+margin.top+')')
			.attr('class',function(d){return d.party})
		.selectAll('circle')
			.data(function(d){ return layout( d3.range(0, d.seats, 1) ); })
		.enter()
			.append('circle')
				.attr('cx',function(d){ return scale(d.position.x); })
				.attr('cy',function(d){ return scale(d.position.y); })
				.attr('r', radius)
}

</script>
</html>
(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-scale')) :
	typeof define === 'function' && define.amd ? define(['exports', 'd3-scale'], factory) :
	(factory((global.d3_iconarray = global.d3_iconarray || {}),global.d3));
}(this, function (exports,d3) { 'use strict';

	function iconArrayLayout() {
		var width = undefined;
		var height = undefined;
		var widthFirst = true;
		var maxDimension = undefined;

		function layout(data){
			//work our missing height, width stuff

			setDimensions(data.length);

			return data.map(function(d,i){
				return {
					data:d,
					position:position(i)
				};
			});
		}

		function position(i){
			if(isNaN(width) || isNaN(height)){ 
				console.log('Warning: width/height undefined') 
				return 0;
			}
			if(widthFirst){
				return {
					x: i % width,
					y: Math.floor( i/width )
				};
			}else{
				return {
					x: Math.floor( i/height ),
					y: i % height
				};
			}
		}

		function setDimensions(l){
			//neither width or height is defined
			if(isNaN(width) && isNaN(height)){
				console.log('no width or height');
				if(widthFirst){ 
					width = Math.ceil( Math.sqrt(l) );
					height = Math.ceil( l / width );
				}else{
					height = Math.ceil( Math.sqrt(l) );
					width = Math.ceil( l / height );
				}
			}else if(isNaN(width)){	//width undefined
				width = Math.ceil( l / height );
			}else if(isNaN(height)){ //height undefined
				height = Math.ceil( l / width );
			}
		}

		layout.maxDimension = function(x){
			var itemPosition = position(x); 
			if(widthFirst){
				var x = Math.max(itemPosition.x, width);
				return Math.max(x, itemPosition.y);
			}
			var y = Math.max(itemPosition.y, height);
			return Math.max(y, itemPosition.x);

		}

		layout.position = function(x){
			return position(x);
		}

		layout.width = function(x){
			if(x === undefined) return width;
			width = x;
			return layout;
		};

		layout.height = function(x){
			if(x === undefined) return height;
			height = x;
			return layout;
		};

		layout.widthFirst = function(b){
			if(b === undefined) return widthFirst;
			widthFirst = b;
			return layout;
		};

		return layout;
	};

	function iconArrayScale(){

		var domain = [0,100];
		var range = [0,100];
		var gapInterval = 10;
		var gapSize = 0; //default no change
		var notionalScale = d3.scaleLinear()
								.domain(domain)
								.range(range);

		function scale(domainValue){
			var rangeValue = 20;
			var adjustedDomainValue = domainValue + Math.floor(domainValue/gapInterval)*gapSize;
			//console.log(notionalScale.domain());
			return rangeValue = notionalScale(adjustedDomainValue);		
		}

		function rescale(){
			//calculate an adjusted domain
			var domainLength = (domain[1] - domain[0]) * gapSize;
			var gaps = Math.ceil( domainLength/ gapInterval );
			var adjustedDomain = [ domain[0], domain[1] + gaps ];

			//calculate an adjusted range

			notionalScale.domain(adjustedDomain)
					.range(range);
		}

		scale.gapInterval = function(x){
			if(!x) return gapInterval;
			gapInterval = x;
			rescale();
			return scale;
		};

		scale.gapSize = function(x){
			if(isNaN(x)) return gapSize;
			gapSize = x;
			rescale();
			return scale;
		}

		scale.domain = function(array){
			if(!array) return domain;
			domain = array;
			rescale();
			return scale;
		};

		scale.range = function(array){
			if(!array) return range;
			range = array;
			rescale();
			return scale;
		};

		rescale();
		return scale; 
	}

	var version = "0.0.1";

	exports.version = version;
	exports.layout = iconArrayLayout;
	exports.scale = iconArrayScale;

}));