block by emeeks c42968993536f921a5c8

Sketchy World

Full Screen

A non-photorealistically rendered, purposefully oversimplified map of the world using (d3.sketchy)[https://github.com/sebastian-meier/d3.sketchy] along with some custom sketchy functions. Also implements (Jason Davies’ automatic coloration based on neighbors)[http://bl.ocks.org/jasondavies/4188334].

index.html

<html>
<head>
  <title>D3 in Action Chapter 7 - Example 1</title>
  <meta charset="utf-8" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.min.js" charset="utf-8" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/topojson/1.6.20/topojson.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-geo-projection/0.2.9/d3.geo.projection.min.js"></script>
<script src="d3.sketchy.js"></script>
</head>
<style>
  svg {
    height: 500px;
    width: 900px;
    border: none;
  }
  
  .countries {
    fill: #FF99CC;
    stroke: none;
  }

</style>
<body>

<div id="viz">
  <svg>
  </svg>
</div>
<div id="controls" />
</body>
  <footer>

<script>
  d3.json("simpleworld.json", createMap);

  function createMap(world) {
    var colorScale = d3.scale.ordinal().range(["#f7fbff","#deebf7","#c6dbef","#9ecae1","#6baed6","#4292c6","#2171b5","#08519c","#08306b"]);
    var countries = topojson.feature(world, world.objects.world);
    var neighbors = topojson.neighbors(world.objects.world.geometries);
    var projection = d3.geo.armadillo()
    .scale(260)
    .translate([440, 360])
    .parallel(20)
    .rotate([-12, 0])
    .precision(.1);

    var geoPath = d3.geo.path().projection(projection);

var svg = d3.select("svg");
var defs = svg.append("defs");

var sketchy = d3sketchy();

      var line = d3.svg.line()
        .x(function(d) { return d.x; })
        .y(function(d) { return d.y; });
d3.select("svg").append("g").attr("class", "bg");
d3.select("svg").append("g").attr("class", "fill");
d3.select("svg").append("g").attr("class", "border");

d3.select("g.bg").selectAll("path").data(countries.features)
.enter()
.append("path")
.attr("d", geoPath)
.attr("class", "countries")
.each(function (d, i) {

  d.color = d3.max(
          neighbors[i], function(n) {return countries.features[n].color; }) + 1 | 0;

  var originalNode = this;
  d.geometry.coordinates.forEach(function (p) {
    if (p.length < 2) {
      p = p[0];
    }
    var projectedArea = [];
      p.forEach(function (coordinate) {
        var proj = projection(coordinate);
        projectedArea.push({x: proj[0], y: proj[1]});
      })
      if (d.id !== "ATA") {
        d3.select("g.fill").insert("path", "path")
        .attr("class", "sketchy-fill")
        .style("fill", "none")
        .style("stroke-width", "2px")
        .style("stroke-linecap", "round")
        .attr("stroke", colorScale(d.color))
        .attr("d", cheapSketchy(originalNode));

        sketchy.pathStroke({svg: d3.select("g.border"), path:projectedArea, density:3, sketch:2});
      }
      else {
        d3.select(originalNode).remove();
      }
    })
})

d3.select("g.fill").selectAll("path")
  .attr("stroke-dasharray", function () {return "0," + this.getTotalLength()})
  .transition()
  .delay(function (d,i) {return i * 50})
      .transition()
      .duration(2500)
      .attrTween("stroke-dasharray", tweenDash);

d3.select("g.border").selectAll("path")
  .style("fill", "none")
  .style("stroke", "black")
  .style("stroke-width", 1)
  .attr("stroke-dasharray", function () {return "0," + this.getTotalLength()})
  .transition()
  .delay(function (d,i) {return i * 50})
      .transition()
      .duration(7500)
      .attrTween("stroke-dasharray", tweenDash);

}

function tweenDash() {
  var l = this.getTotalLength(),
      i = d3.interpolateString("0," + l, l + "," + l);
  return function(t) { return i(t); };
}

function cheapSketchy(path) {
  var length = path.getTotalLength();
  var drawCode = "";
  var x = 0;
  var step = 2.5;

  while (x < length / 2) {
    var start = path.getPointAtLength(x);
    var end = path.getPointAtLength(length - x);

    drawCode += " M" + (start.x + (Math.random() * step - step/2)) + " " + (start.y + (Math.random() * step - step/2)) + "L" + (end.x + (Math.random() * step - step/2)) + " " + (end.y + (Math.random() * step - step/2));

    x += step + (Math.random() * step);
  }

  return drawCode;
}
</script>
  </footer>

</html>

d3.sketchy.js

/*global d3:false */
/*jshint unused:false*/

/**
 * Initiate the sketchy library
 * @constructor
 */
var d3sketchy = function(){

	/**
	* Default attributes for generating the shapes, doing this we don't need to check if all parameters are provided
	* And if someone wants to build a lot of shapes with the same properties she just needs to use "setDefaults" to change them
	* @type {object}
	* @defaultvalue
	*/
	var defaults = {
		x:0,
		y:0,
		width:20,
		height:20,
		sketch:1,
		density:1,
		radius:10,
		angle:45,
		count:2,
		shape:"circle",
		clip:"",
		margin:2
	};

	/**
	* Changing the default attributes
	* @param {object} opts - object with default attributes see "var defaults"
	* @return {object} defaults - the full default object
	*/
	function setDefaults(opts){
		defaults = extend(defaults, opts);
		return defaults;
	}

	/**
	* merging two objects, source will replace duplicates in destination
	* @param {object} destination
	* @param {object} source
	*/
	function extend(destination, source) {
		var returnObj = {}, attrname;
		for (attrname in destination) { returnObj[attrname] = destination[attrname]; }
		for (attrname in source) { returnObj[attrname] = source[attrname]; }
		return returnObj;
	}

	/**
	* Generate random number between min and max
	* @param {float|int} min
	* @param {float|int} max
	* @return {float}
	*/
	function rand(min, max){
		return Math.random()* (max-min) + min;
	}

	/**
	* Create sketchy
	* @constructor
	*/
	function sketchy(){
	}

	/**
	* drawing a sketchy line
	* this is kind of the heart of the whole tool.
	* so if you want to make changes to the appearance of the lines, tweak the following lines
	* @param {object} opts
	* @param {d3.selection} opts.svg
	* @param {float|int} opts.x1 - x point 1
	* @param {float|int} opts.y1 - y point 1
	* @param {float|int} opts.x2 - x point 2
	* @param {float|int} opts.y2 - y point 2
	* @param {object} opts.sketch
	* @param {object} opts.sketch.x - sketchiness on the x-axis
	* @param {object} opts.sketch.y - sketchiness on the y-axis
	*/
	sketchy.drawLine = function(opts){
		//Each line is drawn twice the increase sketchiness
		for(var i = 1; i<3; i++){

			var or2 = rand(0.2, 0.8);

			var cx2 = opts.x1+ (opts.x2-opts.x1)*or2+rand(-1,1);
			var cy2 = opts.y1+ (opts.y2-opts.y1)*or2+rand(-1,1);

			var or1 = or2 + rand(-0.3, -0.2);

			var cx1 = opts.x1+ (opts.x2-opts.x1)*or1+rand(-1,1);
			var cy1 = opts.y1+ (opts.y2-opts.y1)*or1+rand(-1,1);

			opts.svg.append("path")
				.attr("d", "M"+
								(opts.x1 + rand(-1,0)*opts.sketch.x/i)+" "+
								(opts.y1 + rand(-1,1)*opts.sketch.y/i)+" Q"+
								cx1+" "+cy1+" "+
								cx2+" "+cy2+" T"+
								(opts.x2 + rand(0,1)*opts.sketch.x/i)+" "+
								(opts.y2 + rand(-1,1)*opts.sketch.y/i));

		}
	};

	sketchy.drawLineSinglePath = function(opts){
		//Each line is drawn twice the increase sketchiness
		var sketching = "";
		for(var i = 1; i<3; i++){

			var or2 = rand(0.2, 0.8);

			var cx2 = opts.x1+ (opts.x2-opts.x1)*or2+rand(-1,1);
			var cy2 = opts.y1+ (opts.y2-opts.y1)*or2+rand(-1,1);

			var or1 = or2 + rand(-0.3, -0.2);

			var cx1 = opts.x1+ (opts.x2-opts.x1)*or1+rand(-1,1);
			var cy1 = opts.y1+ (opts.y2-opts.y1)*or1+rand(-1,1);

			sketching += " M"+
								(opts.x1 + rand(-1,0)*opts.sketch.x/i)+" "+
								(opts.y1 + rand(-1,1)*opts.sketch.y/i)+" Q"+
								cx1+" "+cy1+" "+
								cx2+" "+cy2+" T"+
								(opts.x2 + rand(0,1)*opts.sketch.x/i)+" "+
								(opts.y2 + rand(-1,1)*opts.sketch.y/i);

		}
		return sketching;
	};

	/**
	* drawing a circle shape
	* no outline just the fill
	* @param {object} opts - object containing the attributes
	* @param {float|int} opts.x - x position
	* @param {float|int} opts.y - y position
	* @param {float|int} opts.r - radius
	* @param {float|int} opts.angle - angle of the lines (0-360)
	* @param {float|int} opts.density - distance between lines
	* @param {float|int} opts.sketch - sketchiness factor
	* @param {string} opts.shape - this is a development relic, default is "circle", alternatives "cut" and "star"
	* @return {object} svg - d3.selection of a group object, containing the circle
	*/
	sketchy.circleFill = function(opts){
		//merging default attributes with user attributes
		var merged_opts = extend(defaults, opts);

		//create a container, this is used to translate and rotate the circle, this container will be returned at the end of this function
		var svg = merged_opts.svg.append("g").attr("transform", "translate("+merged_opts.x+" "+merged_opts.y+") rotate("+merged_opts.angle+")");

		var fillLines = "";

		//Looping through the lines
		var y_dist = 0;
		while(y_dist > -2*opts.r){
			var x;
			//During the development i accidentaly generated those shapes and kept them :)
			if(merged_opts.shape==="cut"){
				x = Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
			}else if(merged_opts.shape==="star"){
				x = merged_opts.r - Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
			}else{
				x = Math.sqrt( ( Math.pow(merged_opts.r, 2) - Math.pow((merged_opts.r-Math.abs(y_dist)), 2) ) );
			}

			//Draw the sketchy lines
			fillLines += sketchy.drawLineSinglePath({
				svg:svg,
				x1:-x,
				y1:y_dist+merged_opts.r,
				x2:x,
				y2:y_dist+merged_opts.r,
				sketch:{
					x:merged_opts.density*merged_opts.sketch,
					y:merged_opts.density*merged_opts.sketch
				}
			});

			y_dist -= merged_opts.density;
		}

		svg.append("path").attr("d", fillLines);

		return svg;
	};

	/**
	* draws a rectangle
	* no outline just the fill
	* @param {object} opts - object containing the attributes
	* @param {float|int} opts.x - x position
	* @param {float|int} opts.y - y position
	* @param {float|int} opts.width - width
	* @param {float|int} opts.height - height
	* @param {float|int} opts.angle - angle of the lines (0-360)
	* @param {float|int} opts.density - distance between lines
	* @param {float|int} opts.sketch - sketchiness factor
	* @return {object} svg - d3.selection of a group object, containing the rectangle
	*/
	sketchy.rectFill = function(opts){
		var svg = opts.svg.append("g").attr("transform", "translate("+opts.x+" "+opts.y+")");
		opts.svg = svg;
		return sketchy.drawPattern(opts);
	};

	/**
	* draws a background pattern in the shape of a square according to x,y,with,height
	* @param {object} opts - object containing the attributes
	* @param {float|int} opts.x - x position
	* @param {float|int} opts.y - y position
	* @param {float|int} opts.width - width
	* @param {float|int} opts.height - height
	* @param {float|int} opts.angle - angle of the lines (0-360)
	* @param {float|int} opts.density - distance between lines
	* @param {float|int} opts.sketch - sketchiness factor
	* @return {object} svg - d3.selection of a group object, containing the background
	*/
	sketchy.drawPattern = function(opts){
		var svg = opts.svg;

		var drawCode = "";

		//angle for strokes
		var angle = opts.angle;
		while(angle > 360){angle -= 360;}
		if(angle > 180){angle -= 180;}
		var radian = (Math.PI/180)*(90-angle);
		var vector = {
			y:1,
			x:-1/Math.tan(radian)
		};

		//distance between strokes
		var dist = opts.density;

		var vy, tx, ty, vx, y1, x1, y_dist, x_dist;

		opts.x = 0;
		opts.y = 0;

		var x = opts.x, y = opts.y;
		if(Math.abs(angle) === 90){
			while(y < opts.y+opts.height){
				drawCode += sketchy.drawLineSinglePath({
					svg:svg,
					x1:x,
					y1:y,
					x2:x+opts.width,
					y2:y,
					sketch:{
						x:dist*opts.sketch,
						y:dist*opts.sketch
					}
				});
				y += dist;
			}
		}else if((Math.abs(angle) === 180)||(angle === 0)){
			while(x < opts.x+opts.width){
				drawCode += sketchy.drawLineSinglePath({
					svg:svg,
					x1:x,
					y1:y,
					x2:x,
					y2:y+opts.height,
					sketch:{
						x:dist*opts.sketch,
						y:dist*opts.sketch
					}
				});
				x += dist;
			}
		}else if(angle < 90){
			y_dist = Math.abs(dist / Math.sin(Math.PI/180*angle));
			x_dist = Math.abs(dist / Math.sin(Math.PI/180*(90-angle)));

			y += y_dist;
			y1 = opts.y;
			x1 = opts.x;
			while(y1 < opts.y+opts.height){
				vx = opts.width / vector.x;
				x1 = opts.width + x;
				y1 = y + vector.y * vx;
				ty = y;
				tx = x;

				if(y1<opts.y){
					vy = (y-opts.y)/vector.y;
					x1 = x + Math.abs(vector.x) * vy;
					y1 = opts.y;
				}else if(y > (opts.y+opts.height)){
					ty = opts.y+opts.height;
					vy = (ty-y1)/vector.y;
					tx = x + opts.width - vy*Math.abs(vector.x);
				}

				drawCode += sketchy.drawLineSinglePath({
					svg:svg,
					x1:tx,
					y1:ty,
					x2:x1,
					y2:y1,
					sketch:{
						x:x_dist*opts.sketch,
						y:y_dist*opts.sketch
					}
				});

				y += y_dist;
			}

		}else{
			y_dist = Math.abs(dist / Math.sin(Math.PI/180*angle));
			x_dist = Math.abs(dist / Math.sin(Math.PI/180*(180-angle)));

			y = opts.y+opts.height;
			y -= y_dist;
			y1 = opts.y+opts.height;
			x1 = opts.x;
			while(y1 > opts.y){
				vx = opts.width / vector.x;
				x1 = opts.width + x;
				y1 = y + vector.y * vx;
				ty = y;
				tx = x;

				if(y1>(opts.y+opts.height)){
					vy = (y-(opts.y+opts.height))/vector.y;
					x1 = x + Math.abs(vector.x * vy);
					y1 = opts.y+opts.height;
				}else if(y < opts.y){
					ty = opts.y;
					vy = (ty-y1)/vector.y;
					tx = x + opts.width - Math.abs(vy*vector.x);
				}

				drawCode += sketchy.drawLineSinglePath({
					svg:svg,
					x1:tx,
					y1:ty,
					x2:x1,
					y2:y1,
					sketch:{
						x:x_dist*opts.sketch,
						y:y_dist*opts.sketch
					}
				});

				y -= y_dist;
			}
		}

		svg.append("path")
		.attr("d", drawCode);

		return svg;
	};

	/**
	* draws a background pattern in the shape of a square according to the position and size of the clip-path object
	* @param {object} opts - object containing the attributes
	* @param {string} opts.clip - id of the clip path
	* @param {float|int} opts.angle - angle of the lines (0-360)
	* @param {float|int} opts.density - distance between lines
	* @param {float|int} opts.sketch - sketchiness factor
	* @param {float|int} opts.margin - extra margin for the background
	* @return {object} svg - d3.selection of a group object, containing the background
	*/
	sketchy.fill = function(opts){
		var merged_opts = extend(defaults, opts);

		var svg = merged_opts.svg.append("g")
			.attr("clip-path", "url(#"+merged_opts.clip+")");

		//Get the bounding box of the object that wants a background
		var bb = d3.select("#"+merged_opts.clip).node().getBBox();

		//To make sure that the background covers the whole are we increase the background by a few pixels
		merged_opts.x = bb.x-merged_opts.margin;
		merged_opts.y = bb.y-merged_opts.margin;
		merged_opts.width = bb.width + 2*merged_opts.margin;
		merged_opts.height = bb.height + 2*merged_opts.margin;
		merged_opts.svg = svg;

		return sketchy.drawPattern(merged_opts);
	};

	/**
	* draws a background pattern in the shape of a square according to the position and size of the clip-path object
	* @param {object} opts - object containing the attributes
	* @param {array} opts.path - array of points {x:float|integer, y:float|integer}
	* @param {int} opts.count - how many altered paths should be generated
	* @param {float|int} opts.sketch - sketchiness factor
	* @return {array} paths - altered paths
	*/
	sketchy.alterPath = function(opts){
		var merged_opts = extend(defaults, opts);
		var paths = [];
		for(var i = 0; i<merged_opts.count; i++){
			var t_path = [];
			for(var j = 0; j<merged_opts.path.length; j++){
				t_path.push({
					x:merged_opts.path[j].x + rand(-1,1)*merged_opts.sketch/(i+1),
					y:merged_opts.path[j].y + rand(-1,1)*merged_opts.sketch/(i+1)
				});
			}
			paths.push(t_path);
		}
		return paths;
	};

	/**
	* Draws alterPath() paths
	* only straight lines, use alterPath and your own drawing function to draw curves etc.
	* @param {object} opts - object containing the attributes
	* @param {array} opts.svg - d3.selection of an svg
	* @param {array} opts.path - array of points {x:float|integer, y:float|integer}
	* @param {int} opts.count - how many altered paths should be generated
	* @param {float|int} opts.sketch - sketchiness factor
	* @return {object} svg - svg with the strokes
	*/
	sketchy.pathStroke = function(opts){
		var paths = sketchy.alterPath(opts);
		var svg = opts.svg;

		var drawCode = "";
		for(var i = 0; i<paths.length; i++){
			for(var j = 0; j<paths[i].length; j++){
				var x1 = paths[i][j].x;
				var y1 = paths[i][j].y, x2, y2;
				if(j<(paths[i].length-1)){
					x2 = paths[i][j+1].x;
					y2 = paths[i][j+1].y;
				}else{
					x2 = paths[i][0].x;
					y2 = paths[i][0].y;
				}
				drawCode += sketchy.drawLineSinglePath({
					svg:svg,
					x1:x1,
					y1:y1,
					x2:x2,
					y2:y2,
					sketch:{
						x:opts.sketch,
						y:opts.sketch
					}
				});
			}
		}
		svg.append("path")
		.attr("d", drawCode);
		return svg;
	};

	/**
	* Helper function for circleStroke
	* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
	* Generates an altered circle path
	* @param {float|int} radius - radius of the circle
	* @param {float|int} radius_min - alternating radius min
	* @param {float|int} radius_max - alternating radius max
	* @param {float|int} s_angle_min - alternating angle min
	* @param {float|int} s_angle_max - alternating angle max
	* @param {float|int} rotation_min - alternating rotation min
	* @param {float|int} rotation_max - alternating rotation max
	* @return {string} path - altered circle svg path
	*/
	function circlePath(radius, radius_min,radius_max, s_angle_min, s_angle_max, rotation_min,rotation_max) {
		var c = 0.551915024494,
			b = Math.atan(c),
			d = Math.sqrt(c*c+1*1),
			r = radius,
			o = rand(s_angle_min, s_angle_max)*Math.PI/180,
			path = 'M';

		path += [r * Math.sin(o), r * Math.cos(o)];
		path += ' C' + [d * r * Math.sin(o + b), d * r * Math.cos(o + b)];

		for (var i=0; i<4; i++) {
			o += Math.PI/2 * (1 + rand(rotation_min, rotation_max));
			r *= (1 + rand(radius_min, radius_max));
			path += ' ' + (i?'S':'') + [d * r * Math.sin(o - b), d * r * Math.cos(o - b)];
			path += ' ' + [r * Math.sin(o), r * Math.cos(o)];
		}

		return path;
	}

	/**
	* Helper function for circleStroke
	* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
	* Generates the transform value for squashing and rotating
	* @param {float|int} squash_min - squashing min
	* @param {float|int} squash_max - squashing max
	* @param {float|int} squash_rotation_min - squashing rotation min
	* @param {float|int} squash_rotation_max - squashing rotation max
	* @return {string} path - transform string
	*/
	function circleTransform(squash_min, squash_max, squash_rotation_min, squash_rotation_max) {
		var o = rand(squash_rotation_min, squash_rotation_max);
		return 'rotate('+o+')'+'scale(1,'+rand(squash_min, squash_max) + ')';
	}

	/**
	* Draw a sketch circle stroke
	* Adapted from http://codepen.io/spencerthayer/pen/nhjwu
	* @param {object} opts - object containing the attributes
	* @param {object} opts.svg - svg container
	* @param {float|int} opts.x - center x of circle
	* @param {float|int} opts.y - center y of circle
	* @param {float|int} opts.r - radius of circle
	* @param {int} count - number of strokes
	* @param {float|int} sketch - sketchiness factor
	* @return {object} svg - d3.selection of the svg containing the circles
	*/
	sketchy.circleStroke = function(opts){
		var merged_opts = extend(defaults, opts);

		var svg =  merged_opts.svg.append("g").attr('transform', function() { return "translate("+merged_opts.x+" "+merged_opts.y+") "+circleTransform(1,1, 0,360); });

		var drawCode = "";

		for(var i = 0; i<merged_opts.count; i++){
				drawCode += " " + circlePath(merged_opts.r, merged_opts.sketch/-50/(i+1),merged_opts.sketch/10/(i+1), 200,240, 0,merged_opts.sketch/5/(i+1));
		}

		svg.append('path')
			.attr("class", "sketchy-stroke")
			.attr('d', drawCode);

		return svg;
	};

	/**
	* Draw a sketch rectangle stroke
	* @param {object} opts - object containing the attributes
	* @param {object} opts.svg - svg container
	* @param {float|int} opts.x - x coordinate
	* @param {float|int} opts.y - y coordinate
	* @param {float|int} opts.width - width
	* @param {float|int} opts.height - height
	* @param {int} count - number of strokes
	* @param {float|int} sketch - sketchiness factor
	* @return {object} svg - d3.selection of the svg containing the rectangles
	*/
	sketchy.rectStroke = function(opts){
		var merged_opts = extend(defaults, opts);
		var svg = merged_opts.svg.append("g");


		var path = [
			{x:merged_opts.x, y:merged_opts.y},
			{x:merged_opts.x+merged_opts.width, y:merged_opts.y},
			{x:merged_opts.x+merged_opts.width, y:merged_opts.y+merged_opts.height},
			{x:merged_opts.x, y:merged_opts.y+merged_opts.height}
		];

		return sketchy.pathStroke({svg:svg, path:path, count:merged_opts.count, sketch:merged_opts.sketch});
	};

	return sketchy;
};