Visualizing wine data using Choropleths and Linking

4 minute read

Published:

In this post, I will try to visualize some interesting statistics extraced from a wine reviews dataset. For this purpose I have developed two visualizations: one consisting in a choropleth map linked to a bar chart, and another consisting in a scatter plot with variable size (bubble chart). More details on the preprocessing of the data can be found in this Jupyter notebook.

In the first visualization, the world map shows each country with its total number of wines reviewed on WineEnthusiast, while the bar chart shows the average obtained score for each country. The two plots are linked together, and additional details are provided on mouseover.

With the second chart I wanted to dig deeper into the relationship between the price of a bottle, and the score it obtained during the review. As in the first visualization, additional details are available on mouseover.

Here is the source code for the two visualizations:

<!DOCTYPE html>
<meta charset="utf-8">

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.js"></script>


<style>
  .countries {
    fill: none;
    stroke: #fff;
    stroke-linejoin: round;
  }
  .legendThreshold {
      font-size: 12px;
      font-family: sans-serif;
  }
</style>

<svg></svg>

<script>

req = new XMLHttpRequest();
req.open(
    "GET", 
  "https://raw.githubusercontent.com/ClonedOne/exp_nbs/master/res/code_country.json", 
  false
);
req.send();
var codeCountry = JSON.parse(req.responseText);
var xTicks = []

for (let code of Object.keys(codeCountry)){
  xTicks.push(codeCountry[code]);
}


// based on the example at 
// http://bl.ocks.org/palewire/d2906de347a160f38bc0b7ca57721328

// GLOBALS

var width = 900;
var height = 1000;

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


var legendLabels = ['0', '1-5', '6-10', '11-25', '26-100', '101-1000', '1000-10000', '>10000' ];
var intervals = [1, 6, 11, 26, 101, 1001, 10001];
var colorRange = d3.schemeRdPu[8];


// COLORMAP

var colorMap = d3.scaleThreshold()
    .domain(intervals)
    .range(colorRange);


// SVG SETUP

var svg = d3.select("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("id", "mainsvg");

var svgMap =  d3.select("svg#mainsvg")
    .append("svg")
        .attr("id", "svgMap")
        .attr("height", height/2)
        .attr("width", width);

var svgBar = d3.select("svg#mainsvg")
    .append("g")
        .append("svg")
        .attr("id", "svgBar")
        .attr("transform", "translate(0," + (height/2) + ")")
        .attr("height", height/2)
        .attr("width", width);


// MAP

var path = d3.geoPath();

var projection = d3.geoNaturalEarth()
    .scale(width / 2 / Math.PI)
    .translate([width / 2, height / 4]);

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

var data = d3.map();


// BAR CHART

var xScale = d3.scaleBand()
  .domain(xTicks)
  .rangeRound([margin.left, width - margin.right])
  .padding(0.2);

var yScale = d3.scaleLinear()
  .domain([80, 95])
  .range([height/2 - margin.bottom, margin.top]);

var xAxis = svgBar.append("g")
  .attr("transform", `translate(0, ${height/2 - margin.bottom})`)
  .call(d3.axisBottom().scale(xScale))
  .selectAll("text")    
  .style("text-anchor", "end")
  .attr("dx", "-.8em")
  .attr("dy", ".15em")
  .attr("transform", "rotate(-65)");

var yAxis = svgBar.append("g")
  .attr("transform", `translate(${margin.left}, 0)`)
  .call(d3.axisLeft().scale(yScale));
  


// LEGEND

var g = svgMap.append("g")
    .attr("class", "legendThreshold")
    .attr("transform", "translate(20,20)");

var legend = d3.legendColor()
    .labels(function (d) { return legendLabels[d.i]; })
    .shapePadding(4)
    .scale(colorMap);

svgMap.select(".legendThreshold")
    .call(legend);


// TOOTLIP SETUP

var Tooltip = d3.select("body")
    .append("div")
    .style("width", 200)
    .style("position", "absolute")
    .style("text-align", "center")
    .style("color", "#585858")
    .style("opacity", 0)
    .style("background-color", "white")
    .style("border", "solid")
    .style("border-color", colorRange[7])
    .style("border-width", "2px")
    .style("border-radius", "5px")
    .style("padding", "2px");

function mouseover(d, i) {
    d.total = data.get(d.id) || [0, 0];
    
    Tooltip
        .html(
            "Country: " + d.id + 
            "<br> Wines reviewed: " + d.total[0] +
            "<br> Average score: " +
            parseFloat(Math.round(d.total[1] * 100) / 100).toFixed(2) 
        )
        .style("left", (d3.event.pageX + 10) + "px")
        .style("top", (d3.event.pageY + 10) + "px")
        .style("opacity", 1.0);

    d3.selectAll("rect")
        .style("opacity", 0.5);

    d3.selectAll("path")
        .style("opacity", 0.5);

    d3.selectAll("rect.c_" + i)
        .style("stroke", "black")
        .style("opacity", 1.0);

    d3.selectAll("path.c_" + i)
        .style("stroke", "black")
        .style("opacity", 1.0);
};

var mousemove = function(d) {
    Tooltip
        .style("left", (d3.event.pageX + 10) + "px")
        .style("top", (d3.event.pageY + 10) + "px");
};

function mouseleave(d, i) {
    Tooltip
        .style("opacity", 0)

    d3.selectAll("rect")
        .style("opacity", 0.9);

    d3.selectAll("rect.c_" + i)
        .style("stroke", "none");

    d3.selectAll("path")
        .style("opacity", 0.9);

    d3.selectAll("path.c_" + i)
        .style("stroke", "none");
};


// DATA LOADING 

// The script will try to load quite a bit of data, therefore it is designed to
// operate asynchronously

d3.queue()
    .defer(
        d3.json, 
        "http://enjalot.github.io/wwsd/data/world/world-110m.geojson"
    )
    .defer(
        d3.csv, 
        "https://raw.githubusercontent.com/ClonedOne/exp_nbs/master/res/country_wine_data.csv",
        function(d) { data.set(d.code, [+d.count, +d.points]); }
    )
    .await(ready);

// This callback function will draw the map

function ready(error, topography) {
    if (error) throw error;

    // Plotting the map 

    svgMap.append("g")
        .attr("class", "countries")
        .selectAll("path")
        .data(topography.features)
        .enter().
        append("path")
            .attr("fill", function (d){
                d.total = data.get(d.id) || [0, 0];
                return colorMap(d.total[0]);
            })
            .style("opacity", 0.9)
            .attr("d", path)
            .attr("class", function(d,i) { return "c_" + i; })
            .on("mouseover", function(d,i) { mouseover(d, i) })
            .on("mousemove", mousemove)
            .on("mouseleave", function(d,i) { mouseleave(d, i) });

    // Plotting the bar chart
    
    svgBar.selectAll("rect")
        .data(topography.features)
        .enter()
        .append("rect")
            .attr("x", function(d) {
                return xScale(codeCountry[d.id]); 
            })
            .attr("y", function(d) {
                d.total = data.get(d.id) || [0, 0];
                return yScale(d.total[1]);
            })
            .attr("width", xScale.bandwidth())
            .attr("height", function(d){
                d.total = data.get(d.id) || [0, 0];
                return height/2 - margin.bottom - yScale(d.total[1]);
            })
            .attr("fill", function(d) {
                d.total = data.get(d.id) || [0, 0];
                return colorMap(d.total[0])
            })
            .style("opacity", 0.9)
            .attr("class", function(d,i) { return "c_" + i; })
            .on("mouseover", function(d,i) { mouseover(d, i) })
            .on("mousemove", mousemove)
            .on("mouseleave", function(d,i) { mouseleave(d, i) });

}


// TITLE

svgMap.append("text")
    .attr("x", (width / 2))
    .attr("y", 30)
    .style("fill", "#585858")
    .attr("text-anchor", "middle")
    .style("font-size", "16px")
    .style("font-weight", "bold")
    .style("font-family", "sans-serif")
    .text("Wines reviewed per country");

svgBar.append("text")
    .attr("x", (width/ 2))
    .attr("y", 30)
    .style("fill", "#585858")
    .attr("text-anchor", "middle")
    .style("font-size", "16px")
    .style("font-weight", "bold")
    .style("font-family", "sans-serif")
    .text("Average score per country");

</script>

<!DOCTYPE html>
<meta charset="utf-8">

<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.js"></script>

<svg></svg>

<script>

// GLOBALS

var data = [];

var width = 700;
var height = 600;

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

var legendLabels = ['0-100', '101-1000', '1000-5000', '5000-10000', '>10000' ];
var intervals = [101, 1001, 5001,10001];
var colorRange = d3.schemeRdPu[5];


// COLORMAP

var colorMap = d3.scaleThreshold()
    .domain(intervals)
    .range(colorRange);


// SVG SETUP

var svg = d3.select("body")
	.append("svg")
	.attr("width", width)
	.attr("height", height);


// AXIS SETUP

var xScale = d3.scaleLinear()
	.domain([79, 101])
	.rangeRound([margin.left, width - margin.right])

var yScale = d3.scaleLinear()
	.domain([0, 500])
	.range([height - margin.bottom, margin.top]);

var rScale = d3.scaleLog()
    .base(Math.E)
    .domain([1, 2000])
    .range([1, 15]);

var xAxis = svg.append("g")
	.attr("transform", `translate(0, ${height - margin.bottom})`)
	.call(d3.axisBottom().scale(xScale));

var yAxis = svg.append("g")
	.attr("transform", `translate(${margin.left}, 0)`)
	.call(d3.axisLeft().scale(yScale));


// TOOLTIP FUNCTIONS

var Tooltip = d3.select("body")
	.append("div")
		.style("width", 200)
		.style("position", "absolute")			
		.style("text-align", "center")
		.style("opacity", 0)
		.style("background-color", "white")
		.style("border", "solid")
	    .style("border-color", colorRange[4])
		.style("border-width", "2px")
		.style("border-radius", "5px")
		.style("padding", "2px");


var mouseover = function(d) {
	Tooltip
		.html(
			"Review score: " + d[0] +
			"<br>Average price: " +  
			parseFloat(Math.round(d[1] * 100) / 100).toFixed(2) +
			"<br>Number of wines: " + d[2]
		)
		.style("left", (d3.event.pageX + 10) + "px")		
		.style("top", (d3.event.pageY + 10) + "px")
		.style("opacity", 1);

	d3.select(this)
	    .style("stroke", "black")
		.style("opacity", 1);
};

var mousemove = function(d) {
	Tooltip
		.style("left", (d3.event.pageX + 10) + "px")		
		.style("top", (d3.event.pageY + 10) + "px");
};

var mouseleave = function(d) {
	Tooltip
		.style("opacity", 0);

	d3.select(this)
		.style("stroke", "none")
		.style("opacity", 0.8);
};


// LEGEND

var g = svg.append("g")
    .attr("class", "legendThreshold")
    .attr("transform", "translate(80,60)");

var legend = d3.legendColor()
    .labels(function (d) { return legendLabels[d.i]; })
    .shapePadding(4)
    .scale(colorMap);

svg.select(".legendThreshold")
    .call(legend);


// LOADING DATA

d3.queue()
    .defer(
        d3.csv, 
        "https://raw.githubusercontent.com/ClonedOne/exp_nbs/master/res/score_price_wine_data.csv",
        function(d) { data.push([d.score, +d.price, +d.count]); }
    )
    .await(ready);


// PLOTTING

function ready(error) {
	if (error) throw error;

	console.log(data)
	var circles = svg.selectAll('circle')
		.data(data)
		.enter()
			.append('circle')
      		.attr('cx', function (d) { return xScale(d[0]) })
      		.attr('cy', function (d) { return yScale(d[1]) })
      		.attr('r', function (d) { return rScale(d[2]) })
      		.attr('fill', function (d) { return colorMap(d[2]) })
			.style("opacity", 0.8)
      		.on("mouseover", mouseover)
			.on("mousemove", mousemove)
			.on("mouseleave", mouseleave);
}



// AXIS LABELS

svg.append("text")
    .attr("text-anchor", "end")
    .attr("x", width/2 + margin.left)
    .attr("y", height - 6)
    .text("Review score");

svg.append("text")
    .attr("text-anchor", "end")
	.attr("transform", "translate("+ 15 +","+(height/2 - margin.bottom)+")rotate(-90)") 
    .text("Average Price");



// TITLE

svg.append("text")
    .attr("x", (width / 2))
    .attr("y", 30)
    .style("fill", "#585858")
    .attr("text-anchor", "middle")
    .style("font-size", "16px")
    .style("font-weight", "bold")
    .style("font-family", "sans-serif")
    .text("Average Price vs. Review Score with sample size");


</script>