Visualizing wine data using Choropleths and Linking
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>
Enjoy Reading This Article?
Here are some more articles you might like to read next: