block by nbremer 240d682d74f1fe66b3540e419843e54c

Abrupt gradients - The growth of BMI across the world

Full Screen

This example comes from my blog on Using gradients for abrupt color changes in data visualizations.

Each line in this data visualization is filled with a linear gradient (there is one unique SVG gradient) that abruptly changes color to showcase the different categories within the BMI scale.

The chart shows the growing BMI of practically all these 25 countries (a random sample of the 200 available) over the past 40 years. Use the buttons below to switch between Men and Women

The data is from the NCD RisC

Two other examples of using these abrupt color changes can be found here

script.js

///////////////////////////////////////////////////////////////////////////
/////////////////////////////// Initiate SVG //////////////////////////////
///////////////////////////////////////////////////////////////////////////

var margin = {
	top: 160,
	right: 140,
	bottom: 50,
	left: 140
};

var width = Math.max(window.innerWidth - margin.left - margin.right, 500) - 30,
	height = Math.min(window.innerHeight - margin.top - margin.bottom - 100, width*2/3);

//Reset the overall font size
var newFontSize = Math.min(width * 62.5 / 1400, 62.5);
d3.select("html").style("font-size", newFontSize + "%");

//SVG container
var svg = d3.select("#chart")
	.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 + ")");

///////////////////////////////////////////////////////////////////////////
/////////////////////////////// Append titles /////////////////////////////
///////////////////////////////////////////////////////////////////////////

var titleWrapper = svg.append("g").attr("class", "titleWrapper");

//Append title to the top
titleWrapper.append("text")
	.attr("class", "title")
    .attr("x", width/2)
    .attr("y", -100)
    .style("text-anchor", "middle")
    .text("Mean Body Mass Index (BMI) per Country");
titleWrapper.append("text")
	.attr("class", "subtitle")
    .attr("x", width/2)
    .attr("y", -50)
    .style("text-anchor", "middle")
    .text("Men");

///////////////////////////////////////////////////////////////////////////
//////////////////////////////// Create axes //////////////////////////////
///////////////////////////////////////////////////////////////////////////	

var axisWrapper = svg.append("g").attr("class", "axisWrapper");

var xScale = d3.scale.linear()
    .range([0, width])
    .domain(d3.extent(bmi, function(d) { return d.year; }))
   	.nice();

var yScale = d3.scale.linear()
    .range([height, 0])
    .domain(d3.extent(bmi, function(d) { return d.mean_bmi; }))
   	.nice();

var xAxis = d3.svg.axis()
    .scale(xScale)
    .orient("bottom")
    .tickFormat(d3.format("d"));
axisWrapper.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + height + ")")
  .call(xAxis);

var yAxis = d3.svg.axis()
    .scale(yScale)
    .orient("left");
axisWrapper.append("g")
  .attr("class", "y axis")
  .call(yAxis);

///////////////////////////////////////////////////////////////////////////
////////////////////////////// Create legend //////////////////////////////
///////////////////////////////////////////////////////////////////////////	

var legendWrapper = svg.append("g").attr("class", "legendWrapper");

//Legend
	legendWrapper.append("text")
	.attr("class", "axisLegend")
  	.attr("transform", "rotate(-90)")
  	.attr("y", width)
  	.attr("x", -yScale(32))
  	.style("fill", "#D62C2B")
  	.text("Obese");
legendWrapper.append("text")
	.attr("class", "axisLegend")
  	.attr("transform", "rotate(-90)")
  	.attr("y", width)
  	.attr("x", -yScale(27.5))
  	.style("fill", "#F7804B")
  	.text("Overweight");
legendWrapper.append("text")
	.attr("class", "axisLegend")
  	.attr("transform", "rotate(-90)")
  	.attr("y", width)
  	.attr("x", -yScale(22.5))
  	.style("fill", "#9C9C9C")
  	.text("Normal weight");

////////////////////////////////////////////////////////////// 
//////////////////////// Gradients /////////////////////////// 
////////////////////////////////////////////////////////////// 

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

linearGradient = defs.append("linearGradient")
	.attr("id", "gradient-bmi")
	.attr("gradientUnits", "userSpaceOnUse")    
	.attr("x1", 0)
	.attr("y1", 0)         
	.attr("x2", 0)
	.attr("y2", height);

linearGradient.append("stop")
	.attr("class", "left")
	.attr("offset", yScale(30)/yScale.range()[0])
	.attr("stop-color", "#D62C2B");
linearGradient.append("stop")
	.attr("class", "left")
	.attr("offset", yScale(30)/yScale.range()[0])
	.attr("stop-color", "#F7804B");
linearGradient.append("stop")
	.attr("class", "left")
	.attr("offset", yScale(25)/yScale.range()[0])
	.attr("stop-color", "#F7804B");
linearGradient.append("stop")
	.attr("class", "left")
	.attr("offset", yScale(25)/yScale.range()[0])
	.attr("stop-color", "#BABABA");

///////////////////////////////////////////////////////////////////////////
////////////////////////////////// Voronoi ////////////////////////////////
///////////////////////////////////////////////////////////////////////////

//Initiate the voronoi function
var voronoi = d3.geom.voronoi()
    .x(function(d) { return xScale(d.year); })
    .y(function(d) { return yScale(d.mean_bmi); })
    .clipExtent([[0, yScale(35)], [width, yScale(17)]]);

//Prepare the data
var gender = d3.nest()
	.key(function(d) { return d.sex; })
	.entries(bmi);

//Initiate the voronoi group element	
var voronoiGroup = svg.append("g").attr("class", "voronoiWrapper");

//Create a new voronoi map including only the visible points
voronoiGroup.selectAll("path")
	.data(voronoi(gender[0].values))
	.enter().append("path")
	.attr("class", function(d,i) { 
		return "voronoi " + d.point.iso; 
	})
	//.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
	.datum(function(d) { return d.point; })
	.attr("class", "voronoiCells")
	.on("mouseover", mouseoverVor)
	.on("mouseout", mouseoutVor);

	///////////////////////////////////////////////////////////////////////////
//////////////////////////////// Plot Data ////////////////////////////////
///////////////////////////////////////////////////////////////////////////	

var line = d3.svg.line()
    .interpolate("basis")
    .x(function(d) { return xScale(d.year); })
    .y(function(d) { return yScale(d.mean_bmi); });

//Wrapper for all the lines
var countriesWrapper = svg.append("g").attr("class","countryWrapper");

//Prepare the data
var countries = d3.nest()
	.key(function(d) { return d.iso; })
	.key(function(d) { return d.sex; })
	.entries(bmi);

//Create a group for each country
var countryGroup = countriesWrapper.selectAll(".countryGroup")
  	.data(countries, function(d) { return d.key; })
	.enter().append("g")
	.attr("class", function(d) { return "country " + d.key; });

//Draw a line for each country
countryGroup.append("path")
	.attr("class", "country line")
  	//.attr("d", function(d) { return line(d.values[0].values); })
  	.style("stroke", "url(#gradient-bmi)");

//Append the actual text to the right of the last value for each country
countryGroup.append("text")
	.attr("class", "countryName")
  	.datum(function(d) { return {country: d.values[0].values[0].country, valueMen: d.values[0].values[d.values[0].values.length - 1], valueWomen: d.values[1].values[d.values[1].values.length - 1]}; })
  	//.attr("transform", function(d) { return "translate(" + xScale(d.valueMen.year) + "," + yScale(d.valueMen.mean_bmi) + ")"; })
  	.attr("x", 3)
  	.attr("dy", ".35em")
  	.text(function(d) { return d.country; });

///////////////////////////////////////////////////////////////////////////
///////////////////////////// Threshold lines /////////////////////////////
///////////////////////////////////////////////////////////////////////////	

countriesWrapper.append("path")
  	.attr("class", "threshold")
  	.attr("d", "M" + 0 + "," + yScale(25) + " L" + width + "," + yScale(25))
  	.style("stroke", "#F7804B");

countriesWrapper.append("path")
  	.attr("class", "threshold")
  	.attr("d", "M" + 0 + "," + yScale(30) + " L" + width + "," + yScale(30))
  	.style("stroke", "#D62C2B");

///////////////////////////////////////////////////////////////////////////
//////////////////////// Change between datasets //////////////////////////
///////////////////////////////////////////////////////////////////////////	

function changeToMen() { 

	//Change subtitle
	d3.select(".subtitle")
        .text("Men");

	//Change to females
	d3.selectAll(".country")
		.transition().duration(1000)
	     .attr("d", function(d) { return line(d.values[0].values); });
	
	//Move labels to men
	d3.selectAll(".countryName")
      	.attr("transform", function(d) { return "translate(" + xScale(d.valueMen.year) + "," + yScale(d.valueMen.mean_bmi) + ")"; });

    //Create voronois
    setTimeout(function() {
	    d3.selectAll(".voronoiWrapper").selectAll("path")
			.data(voronoi(gender[0].values))
			.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
			.datum(function(d) { return d.point; });
	},1000);

}//men

function changeToWomen() { 

	//Change subtitle
	d3.select(".subtitle")
        .text("Women");

	//Change to females
	d3.selectAll(".country")
		.transition().duration(1000)
	     .attr("d", function(d) { return line(d.values[1].values); });

	//Move labels to women
	d3.selectAll(".countryName")
      	.attr("transform", function(d) { return "translate(" + xScale(d.valueWomen.year) + "," + yScale(d.valueWomen.mean_bmi) + ")"; });

    //Create voronois
    setTimeout(function() {
	    d3.selectAll(".voronoiWrapper").selectAll("path")
			.data(voronoi(gender[1].values))
			.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
			.datum(function(d) { return d.point; });
	},1000);

}//women

///////////////////////////////////////////////////////////////////////////
//////////////////////////// Button Activity //////////////////////////////
///////////////////////////////////////////////////////////////////////////	

d3.select("#menButton").on("click", changeToMen);
d3.select("#womenButton").on("click", changeToWomen);

///////////////////////////////////////////////////////////////////////////
/////////////////////////// Mouseover functions ///////////////////////////
///////////////////////////////////////////////////////////////////////////	

function mouseoverVor(d) {
	//Dim all lines
	d3.selectAll(".line")
		.style("opacity", 0.1);
	//Highlight selected line
	d3.select(".country." + d.iso + " .line")
		.style("stroke-width", 5)
		.style("opacity", 1);
	//Show selected country
	d3.select(".country." + d.iso + " .countryName")
		.style("opacity", 1);
	//Hide legends
	d3.selectAll(".axisLegend")
		.style("opacity", 0.1);
}//mouseover

function mouseoutVor(d) {
	//Remove stylings
	d3.selectAll(".line, .countryName, .axisLegend")
		.style("opacity", null);
	d3.select(".country." + d.iso + " .line")
		.style("stroke-width", null)
		.style("opacity", null);
}//mouseoutVor

changeToMen();

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
		<title>The growth of BMI over the last 40 years</title>
	
		<!-- D3.js -->
		<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
		
		<!-- Google Fonts -->
		<link href='//fonts.googleapis.com/css?family=Open+Sans:300,400' rel='stylesheet' type='text/css'>

		<!-- jQuery -->
		<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
		<!-- Compiled and minified CSS -->
		<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
		<!-- Compiled and minified JavaScript -->
		<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
		
		<style>
			html { font-size: 62.5%; } 

			body {
			  	font-size: 1.2rem;
			  	font-family: 'Open Sans', sans-serif;
			  	font-weight: 400;
			  	text-align: center;
			  	fill: #2B2B2B;
			}
			
		  	.title {
		    	font-size: 3.6rem;
		    	fill: #4F4F4F;
		    	font-weight: 300;
		  	}

		  	.subtitle {
		    	font-size: 2.2rem;
		    	fill: #AAAAAA;
		    	font-weight: 300;
		  	}

			.axis path,
			.axis line {
			  	fill: none;
			  	stroke: #949494;
			  	shape-rendering: crispEdges;
			}

			.x.axis path {
			  	display: none;
			}

			.axis text {
				fill: #383838;
				font-size: 1.6rem;
				font-weight: 300;
			}

			.line {
			  	fill: none;
			  	stroke-width: 1.5px;
			  	opacity: 0.8;
			  	pointer-events: none;
			}

			.axisLegend {
				text-anchor: middle;
				font-size: 2.2rem;
				font-weight: 300;
				opacity: 1;
			}

			.countryName {
				text-anchor: start;
				font-size: 1.6rem;
				font-weight: 300;
				fill: #383838;
				opacity: 0;
			}

			.threshold {
				fill: none;
			  	shape-rendering: crispEdges;
			  	opacity: 0.3;
			  	stroke-dasharray: 8, 5;
			  	/*stroke-width: 1.5px;*/
			  	pointer-events: none;
			}

			.voronoiWrapper path {
		    	fill: none;
		    	pointer-events: all;
		  	}	
		</style>
	</head>
	<body>
	
		<div id = "chart"></div>
		<!-- The buttons -->
		<div id="button" class="btn-group" data-toggle="buttons">
			<label id="menButton" class="btn btn-default active"><input type="radio" class="btn-options"> Men </label>
			<label id="womenButton" class="btn btn-default"><input type="radio" class="btn-options"> Women </label>
		</div>

		<script src = "bmiSmall.js"></script>
		<script src = "script.js"></script>


  </body>
</html>