block by micahstubbs 29b4fd2f1ed6ed68a81e

Close Votes UK Prototype

Full Screen

a rough prototype for the Close Votes UK election datavis design exercise

index.html

<html>
	<head>
		<script src="https://d3js.org/d3.v3.js"></script>
		<script src="https://d3js.org/queue.v1.min.js"></script>
		<link href='https://fonts.googleapis.com/css?family=Quicksand: 400, 700' rel='stylesheet' type='text/css'>
		<meta property="og:title" content="CLOSE VOTES UK">
		<meta property="og:image" content="//tulpinteractive.com/projects/close-votes/close_votes_large.png" /> 
		<meta property="og:description" content="Based on the 2015 parliamentary elections in United Kingdom, this visualization shows which cities distribute their votes similarly over the political parties" />
		<title>CLOSE VOTES UK</title>
		<style>
			/*body { background-color: #DFCCA5; }*/
			body {
				font-family: 'Quicksand', sans-serif;
				font-size: 11px;
				text-align: center;
				background: rgb(240,249,255); /* Old browsers */
				background: -moz-radial-gradient(center, ellipse cover,  rgba(240,249,255,1) 0%, rgba(203,235,255,1) 47%, rgba(174, 218, 288,1) 100%); /* FF3.6+ */
				background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(240,249,255,1)), color-stop(47%,rgba(203,235,255,1)), color-stop(100%,rgba(174, 218, 288,1))); /* Chrome,Safari4+ */
				background: -webkit-radial-gradient(center, ellipse cover,  rgba(240,249,255,1) 0%,rgba(203,235,255,1) 47%,rgba(174, 218, 288,1) 100%); /* Chrome10+,Safari5.1+ */
				background: -o-radial-gradient(center, ellipse cover,  rgba(240,249,255,1) 0%,rgba(203,235,255,1) 47%,rgba(174, 218, 288,1) 100%); /* Opera 12+ */
				background: -ms-radial-gradient(center, ellipse cover,  rgba(240,249,255,1) 0%,rgba(203,235,255,1) 47%,rgba(174, 218, 288,1) 100%); /* IE10+ */
				background: radial-gradient(ellipse at center,  rgba(240,249,255,1) 0%,rgba(203,235,255,1) 47%,rgba(174, 218, 288,1) 100%); /* W3C */
				filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f0f9ff', endColorstr='#a1dbff',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */
			}	
			ul { list-style: none; padding: 0; margin: 0; display: inline-block; position: absolute; z-index: 100; right: 0px; top: 30px; }
			select { position: absolute; right: 0px; top: 924px; }
			a:hover { color: #F9786C; text-decoration: underline; }	
			h1 { font-size: 32px; margin-bottom: 0;}	
			h2 { font-size: 18px; margin-top: 0; margin-bottom: 0; }
			h4 { margin: 0 auto; font-weight: normal; width: 500px; }
			li { text-align: left; }
			li span { width: 50px; display: inline-block; text-align: right; margin-right: 5px; }
			.selected { color: #000; font-weight: bold; text-decoration: none; cursor: default; }
			.selected:hover { text-decoration: none; color: #000; }
			.notselected { font-weight: normal; text-decoration: underline; cursor: pointer; color: #91B6D4; }
			.notselected:hover { color: #F9786C; }
			#viz { width: 100%; }
			#selectorcontainer { width: 1000px; text-align: right; margin: auto auto; position: relative; } 
			#credit { position: absolute; top: 924px; left: 0; font-size: 9px; }
			#credit a, #credit a:link, #credit a:active { font-weight: normal; text-decoration: none; cursor: pointer; color: #000; }
			#credit a:hover { color: #F9786C; }
		</style>
	</head>
	<body>
		<h1>CLOSE VOTES</h1>
		<h2>Which cities vote like yours?</h2>
		<h4>Based on the 2015 parliamentary elections in The United Kingdom, this visualization shows which cities distribute their votes similarly over the political parties</h4>
		<div id="selectorcontainer">
			<ul>
				<li><span>size:</span><a href="Javascript: void(0);" id="size_by_pop" class="notselected">population</a> / <a href="Javascript: void(0);" id="size_by_similarity" class="selected">similarity</a></li>
				<li><span>layout:</span><a href="Javascript: void(0);" id="layout_radial" class="notselected">radial</a> / <a href="Javascript: void(0);" id="layout_geo" class="selected">geographical</a></li>
			</ul>
			<span id="credit">inspired by <a href="//tulpinteractive.com">TULP interactive</a></span>
		</div>		
		<div id="viz"></div>
		<script type="text/javascript">

queue()
	.defer(d3.json, 'data.json')
	.defer(d3.json, 'parties.json')
	.await(vis); // function that uses files

function vis(error, data, parties) {

	var index = Math.round(Math.random() * data.length),
			w = 1000, 
			h = 1000;


	// create the drop down menu of cities
	var selector = d3.select("#selectorcontainer")
		.append("select")
		.attr("id", "metroselector")
		.selectAll("option")
		.data(data)
		.enter().append("option")
		.text(function(d) { return d.metro; })
		.attr("value", function (d, i) {
			return i;
		});

	d3.select("#metroselector").property("selectedIndex", index);
//27861
	var rl = d3.scale.linear().domain([0, 75]).range([0, w / 2 - 60]);
	var rs = d3.scale.log().domain([1, 74]).range([0, w / 2 - 60]);
	var rr = d3.scale.linear().domain([Math.sqrt(1 / Math.PI), Math.sqrt(15 / Math.PI), Math.sqrt(74 / Math.PI)]).range([22, 5, 2]);
	//var rr = d3.scale.linear().domain([Math.sqrt(1 / Math.PI), Math.sqrt(74 / Math.PI)]).range([22, 2])
	
	var rpopulation = d3.scale.linear().domain(d3.extent(data, function(d) { return Math.sqrt(d.pop / Math.PI); })).range([2, 40])
	// set the domain equal to the observed min and max in the results.json dataset
	var ro = d3.scale.linear().domain([68,316] /* d3.extent(data,  function(d) { return d.opk; }) */ ).range([2, 70])
	var c = d3.scale.log().domain([1, 20, 74]).range(["#F9786C", "#41415F", "#193244"]);

	var axes = [
		{ 'label': 'very similar', 'value': 15 },
		{ 'label': 'similar', 'value': 30 },
		{ 'label': 'different', 'value': 45 },
		{ 'label': 'very different', 'value': 60 },
		{ 'label': 'extremely different', 'value': 75} 
	];

	var rfuncs = [
		function(d, i) { return rpopulation(Math.sqrt(d.pop / Math.PI)); }, // population
		function(d, i) { return rr(Math.sqrt(d['eud'][index] / Math.PI) == 0 ? 1 : Math.sqrt(d['eud'][index] / Math.PI)); }, // similarity
		// hardcoding a common value for opk from the results.json dataset
		// want to find out how to calculate 'opk' for the new dataset
		function(d, i) { return ro(80/*d.opk*/); } // show up
	]

	var rfunc = rfuncs[1];
	var layout = "geo";
	var size = "sim";

	var svg = d3.select("#viz").append("svg")
		.attr("width", w)
		.attr("height", h);

	var merc = d3.geo.mercator();
	merc.translate([140, 3100]);
	merc.scale(2900);

	var selectedmetro = svg.append("text")
		.attr("id", "selectedmetro")
		.attr("x", 74)
		.attr("y", 30)
		.text(data[index]['metro'])
		.style("fill", "#000")
		.style("text-anchor", "start")
		.style("font-family", "Quicksand")
		.style("font-size", "24px");

	var selectors = svg.append("foreignObject")
		.attr("transform", "translate(200, 200)")
		.append("body")
		.append("ul");

	selectors.append("li");
	selectors.append("li");

	var g = svg.append('g')
		.attr('transform', 'translate(' + w / 2 + ', ' + h / 2 + ')');

	var arc = d3.svg.arc()
		.outerRadius(function(d) { return rl(d.value); })
		.startAngle(0)
		.endAngle(2 * Math.PI);

	var ga = g.append("g")
		.attr("id", "axisgroup")
		.style("opacity", 0);

	ga.selectAll(".axispath")
	  	.data(axes)
	  	.enter().append("path")
	  	.attr("id", function(d, i) { return "axispath" + i; })
	  	.attr("class", "axispath")
	  	.attr("d", arc)
	  	.style("stroke", "#91B6D4")
	  	.style("fill", "none")
	  	.style("opacity", 0.6);

	ga.selectAll(".axislabel")
		.data(axes)
		.enter().append("text")
		.attr("class", "axislabel")
		.attr("dy", -5)
		.attr("dx", 0)
		.style("fill", "#91B6D4")
		.style("font-size", "11px")
		.style("text-anchor", "middle")
	  .append("textPath")
	  	.attr("xlink:href", function(d, i) { return "#axispath" + i; })
	  	.attr("startOffset", "40%")
		.text(function(d, i) { return d.label; }) 

	d3.selection.prototype.moveToFront = function() { 
	    return this.each(function() { 
	        this.parentNode.appendChild(this); 
	    }); 
	}; 

	data.forEach(function(d, i) {
		d.s = i;
	})

	d3.select("#metroselector")
		.on("change", function(d) {
			index = this.value;
			update();
		})

	d3.select("#size_by_pop")
		.on("click", function() {
			rfunc = rfuncs[0];
			circle.each(function(d, i) {
				d3.select(this)
					.transition()
					.duration(800)
					.attr('r', rfunc)
			})

			d3.select(this)
				.attr("class", "selected")

			d3.select("#size_by_similarity")
				.attr("class", "notselected")

			size = "pop";
		})

	d3.select("#size_by_similarity")
		.on("click", function() {
			rfunc = rfuncs[1];
			circle.each(function(d, i) {
				d3.select(this)
					.transition()
					.duration(800)
					.attr('r', rfunc)
			})

			d3.select(this)
				.attr("class", "selected")

			d3.select("#size_by_pop")
				.attr("class", "notselected")

			size = "sim";
		})

	d3.select("#layout_radial")
		.on("click", function() {
			layout = "radial";

			d3.selectAll(".metro")
				.transition()
				.duration(1300)
				.attr("cy", 0)
  				.attr("cx", function(d, i) { return rs(d['eud'][index] == 0 ? 1 : d['eud'][index])})
  				.attr("transform", function(d, i) { return "rotate(" + d.s/data.length * 360 + " 0 0)"; })

			d3.selectAll("#axisgroup")
  				.transition()
  				.duration(1300)
  				.style("opacity", 1)

  			d3.select(this)
				.attr("class", "selected")

			d3.select("#layout_geo")
				.attr("class", "notselected")
		})

	d3.select("#layout_geo")
		.on("click", function() {
			layout = "geo";

			d3.selectAll(".metro")
				.transition()
				.duration(1300)
				.attr('cy', function(d) { return merc([d['long'], d['lat']])[1]; })
  				.attr('cx', function(d) { return merc([d['long'], d['lat']])[0]; })
  				.attr("transform", function(d, i) { return "rotate(0 0 0)"; })

  			d3.selectAll("#axisgroup")
  				.transition()
  				.duration(1300)
  				.style("opacity", 0)

  			d3.select(this)
				.attr("class", "selected")

			d3.select("#layout_radial")
				.attr("class", "notselected")
		})

  function update() {
  	if (layout == "radial") {
        	d3.selectAll('.metro')
	          .transition()
	          .duration(800)
	          .attr("cx", function(d, i) { return rs(d['eud'][index] == 0 ? 1 : d['eud'][index])})
	          .attr('r', rfunc)
	          .style('fill', function(d, i) { return index == d.s ? "#F9786C" : c(d['eud'][index] == 0 ? 1 : d['eud'][index]); })	
        } else {
        	d3.selectAll('.metro')
	          .transition()
	          .duration(800)
	          .attr('r', rfunc)
	          .style('fill', function(d, i) { return index == d.s ? "#F9786C" : c(d['eud'][index] == 0 ? 1 : d['eud'][index]); })
        }

        d3.selectAll(".label")
        	.remove();

        d3.select("#selectedmetro")
        	.text(data[index]['metro'])
        
        bar
        	.data(data[index]['voteShare'])
        	.transition()
        	.duration(800)
        	.attr("width", function(d) { return bs(d); })
        
  }		

  var circle = g.selectAll('.metro')
  	.data(data)
  	.enter().append('circle')
    .attr('class', 'metro')
  	.attr('r', rfunc)
  	.attr('cy', function(d) { return merc([d['long'], d['lat']])[1]; })
  	.attr('cx', function(d) { return merc([d['long'], d['lat']])[0]; })
  	.style('fill', function(d, i) { return index == d.s ? "#F9786C" : c(d['eud'][index] == 0 ? 1 : d['eud'][index]); })
  	.style('stroke', 'white')
  	.style('stroke-opacity', 0.3)
  	.style("opacity", 0.9)
    .on("click", function(d, ind) {
    	console.log(d);
    	d3.selectAll('.metro').each(function(d, ind) {
        	if (d.s == index) {
				d3.select(this)
        			.moveToFront()
        	}

        })

        index = ind;
        
        d3.select("#metroselector").property("selectedIndex", index);

        update();

      })
	.on("mouseover", function(d, i) {

		var labelbackground = d3.select(this.parentNode)
			.append('text')
			.attr('class', 'label')
			.style('text-anchor', 'middle')
			.text(function() { return d.metro; })
			.style('font-family', "'Quicksand', sans-serif")
			.style('font-size', '16px')
			.style('font-weight', 'bold')
			.style('stroke', 'rgb(240,249,255)')
			.style('stroke-width', 3.5)
			.style('stroke-opacity', 0.6)
			.style('filter', 'url:(#dropshadow)')
			.attr('dy', function() { return size == "pop" ? -1 * rpopulation(Math.sqrt(d.pop / Math.PI)) - 5 : -1 * rr(Math.sqrt(d['eud'][index] / Math.PI) == 0 ? 1 : Math.sqrt(d['eud'][index] / Math.PI)) - 5; })
			.style('fill', 'none')

		var labelforeground = d3.select(this.parentNode)
			.append('text')
			.attr('class', 'label')
			.style('text-anchor', 'middle')
			.text(function() { return d.metro; })
			.style('font-family', "'Quicksand', sans-serif")
			.style('font-size', '16px')
			.style('font-weight', 'bold')
			.attr('dy', function() { return size == "pop" ? -1 * rpopulation(Math.sqrt(d.pop / Math.PI)) - 5 : -1 * rr(Math.sqrt(d['eud'][index] / Math.PI) == 0 ? 1 : Math.sqrt(d['eud'][index] / Math.PI)) - 5; })
			.style('fill', '#000')

		if (layout == "geo") {
			labelbackground
				.attr('y', function() { return merc([d['long'], d['lat']])[1]; })
  				.attr('x', function() { return merc([d['long'], d['lat']])[0]; })

  			labelforeground
				.attr('y', function() { return merc([d['long'], d['lat']])[1]; })
  				.attr('x', function() { return merc([d['long'], d['lat']])[0]; })
		} else {
			labelbackground
				.attr('x', function() { return index == i ? 0 : Math.cos(d.s/data.length * 2 * Math.PI) * rs(d['eud'][index] == 0 ? 1 : d['eud'][index]); })
				.attr('y', function() { return index == i ? 0 : Math.sin(d.s/data.length * 2 * Math.PI) * rs(d['eud'][index] == 0 ? 1 : d['eud'][index]); })

			labelforeground
				.attr('x', function() { return index == i ? 0 : Math.cos(d.s/data.length * 2 * Math.PI) * rs(d['eud'][index] == 0 ? 1 : d['eud'][index]); })
				.attr('y', function() { return index == i ? 0 : Math.sin(d.s/data.length * 2 * Math.PI) * rs(d['eud'][index] == 0 ? 1 : d['eud'][index]); })
		}

		bg.selectAll(".marker")
			.data(data[d.s]["voteShare"])
			.enter().append("line")
			.attr("class", "marker")
			.attr("x1", function(d) { return 5 + bs(d); })
			.attr("y1", function(d, i) { return i * 11; })
			.attr("x2", function(d) { return 5 + bs(d); })
			.attr("y2", function(d, i) { return 10 + i * 11; })
			.style("stroke", "#000")
			.style("opacity", 0.8)
	})
	.on("mouseout", function(d, i) {
		d3.selectAll('.label')
			.remove()

		d3.selectAll(".marker")
			.remove()
	});

	var bs = d3.scale.linear()
		.domain([0, 100])
		.range([0, 200]);

	var bg = svg.append("g")
		.attr("transform", "translate(200, 50)")


	var party = bg.selectAll(".party")
		.data(parties) // ["VVD", "PvdA", "PVV", "SP", "CDA", "D66", "CU", "GrLinks", "SGP", "PvdD", "50+"]
		.enter().append("text")
		.attr("class", "party")
		.attr("x", 0)
		.attr("y", function(d, i) { return 8 + i * 11; })
		.text(String)
		.style("text-anchor", "end")
		.style("fill", "#91B6D4")

	var bar = bg.selectAll("rect")
		.data(data[index]["voteShare"])
		.enter().append("rect")
		.attr("x", 5)
		.attr("y", function(d, i) { return i * 11; })
		.attr("height", 10)
		.attr("width", function(d) { return bs(d); })
		.style("fill", "#91B6D4")




};    

		</script>
		<script>
/*
		  var _gaq=[['_setAccount','UA-27449759-1'],['_trackPageview']];
		  (function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
		  g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
		  s.parentNode.insertBefore(g,s)}(document,'script'));
*/
		</script>
	</body>
</html>

parties.json

[
  "Conservative",
  "Labour",
  "Scottish National Party",
  "Democratic Unionist Party",
  "Liberal Democrat",
  "Sinn Fein",
  "Plaid Cymru",
  "Social Democratic & Labour Party",
  "Ulster Unionist Party",
  "Green Party",
  "Independent",
  "Other",
  "UKIP",
  "Alliance Party",
  "Cannabis Is Safer Than Alcohol",
  "Christian Peoples Alliance",
  "English Democrats",
  "Monster Raving Loony Party",
  "TUSC"
]