(This chart is a part of the d3-charts
collection available here.)
The subject of this project is nowhere near as exciting as its title would suggest. It aims to present a trend for a political party, or candidate, by combining the polling from multiple institutes as well as their confidence interval.
This chart has two basic views:
The single-party display is the main aim of the chart, but there is support for those of you who want to display multiple parties at once, be it all or a selection of your choice to make a poignant comparison.
This visualization shows the polling for the Danish Social Democrats.
An example of an all-party display can be found here.
Compare this to the trend charts for the Danish Social Democrats by Erik Gahner Larsen and M. Schmidt.
The chart employs a LOESS regression to plot a trend. The LOESS function currently has a bandwidth value of .2
.
var dataset = "https://data.ndarville.com/danish-polls/data.csv", // "data.csv"
parseDate = d3.time.format("%Y/%m/%d").parse,
dateValue = "Date",
instituteValue = "Polling Firm",
lastElectionDate = "2011-09-15", // ""
nextElectionDate = "2015-09-14", // ""
periods = [
{
"start" : "2015-01-07",
"end" : "2015-02-14",
"color" : "",
"label" : "Hebdo and CPH"
}
], // `periods = []`
periodLabels = false, // true
yAxisTitle = "Votes (%)",
votingThreshold = 2.0,
showDots = true,
showAllParties = false,
recalculateYMax = false;
parties = showAllParties === true ? [] : ["A"];
var ignoreFilter = [
"Lead",
"Red (A+B+F+Ø)",
"Blue (V+O+I+C+K)"
]
// Autoconfig
var singleParty = (showAllParties === false && parties.length === 1) ? true : false,
displayInstitutes = singleParty;
parseDate
refers to the date format---in this case YYYY/MM
.
dateValue
refers to the header of the date column in your data.csv
.
instituteValue
is the same, except for your column of polling institutes.
nextElectionDate
refers to the date of the election, if known. This will change the maximum on the x-axis to said date to contextualize the trends.
lastElectionDate
is the counterpart to nextElectionDate
and sets the minimum of the x-axis.
periods
takes an array of dictionaries of time intervals and displays them on the chart. The example currently shows the period from the Charlie Hebdo shooting to the Copenhagen shooting(s). It supports the following keys:
start
: (no default)end
: (no default)color
: (default steelBlue
)label
: (Short) description to be displayed in the legendperiodLabels
is an option for displaying labels defined for your periods.
showDots
is an option to display dots for all the polls. You should at least use this or a trend chart.
votingThreshold
is for displaying the voting threshold to gain a seat in parliament.
showAllParties
is an option for displaying all parties or a manual selection.
recalculateYMax
is an option for calculating the y-axis maximum based on your one selected party.
parties
is an array to manually enter the parties you want to display.
ignoreFilter
refers to headers of the columns you do not want to be parsed as poll data in your chart.
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
svg {
display: block;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
}
.x.axis path {
display: none;
}
.line {
fill: none;
stroke-width: 1.5px;
}
rect.period {
opacity: 0.2;
}
.election-date-line,
.voting-threshold {
stroke: #000;
stroke-width: 1px;
stroke-dasharray: 5,5;
}
circle.dot {
fill: #FFF !important;
stroke-width: 1.5px !important;
}
/** Grid lines BEGIN */
/** https://www.d3noob.org/2013/01/adding-grid-lines-to-d3js-graph.html */
.grid .tick {
stroke: lightgrey;
opacity: 0.7;
}
.grid path {
stroke-width: 0;
}
/** Grid lines END */
#graph {
/** Layout breaks when minima are removed */
min-width: 440px;
min-height: 400px;
width: 100%;
height: 100%;
max-width: 720px;
max-height: 655px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="graph"></div>
<!-- <script src="https://d3js.org/d3.v3.min.js"></script> -->
<script src="d3.min.js?v=3.5.5"></script>
<script src="science.v1.min.js?v=1.9.1"></script>
<script type="text/javascript" charset="utf-8">
// Settings
// ========
var defaultWidth = 440,
defaultHeight = 400,
aspectRatio = defaultWidth/defaultHeight; // = 1.1
var wrapperWidth = parseInt(d3.select("#graph").style("width"), 10),
wrapperHeight = parseInt(d3.select("#graph").style("height"), 10);
var padding = 30,
margin = {
"top" : padding,
"right" : 125,
"bottom" : padding,
"left" : padding
};
// If landscape mode or square
if (wrapperWidth >= wrapperHeight) {
var height = wrapperHeight,
width = Math.round(height * aspectRatio);
// If protrait mode
} else {
var width = wrapperWidth,
height = Math.round(width / aspectRatio);
}
margin.hor = margin.left + margin.right;
margin.ver = margin.top + margin.bottom;
// Config
// ======
var dataset = "https://data.ndarville.com/danish-polls/data.csv", // "data.csv"
parseDate = d3.time.format("%Y-%m-%d").parse,
dateValue = "Date",
instituteValue = "Polling Firm",
lastElectionDate = "2011-09-15", // ""
nextElectionDate = "2015-06-18", // ""
periods = [
{
"start" : "2015-01-07",
"end" : "2015-02-14",
"color" : "",
"label" : "Hebdo and CPH"
},
{
"start" : "2015-05-27",
"end" : "2015-06-18",
"color" : "",
"label" : "Election"
}
], // `periods = []`
periodLabels = false, // `true`
yAxisTitle = "Votes (%)",
votingThreshold = 2.0, // `false`
showDots = true,
showAllParties = false,
recalculateYMax = false;
parties = showAllParties === true ? [] : ["A"];
var ignoreFilter = [
"Lead",
"Red (A+B+F+Ø+Å)",
"Blue (V+O+I+C+K)"
];
// Autoconfig
// ==========
var singleParty = (showAllParties === false && parties.length === 1) ? true : false,
displayInstitutes = singleParty;
ignoreFilter.push(dateValue);
ignoreFilter.push(instituteValue);
// Definitions
// ===========
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var instituteColor = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(function(d) { // ! Ought display all months as single letter
if (d3.time.format("%-b")(d) === "Jan") {
return d3.time.format("%Y")(d);
} else { return d3.time.format("%-b")(d); }
});
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(function(d) { return d + "%"; });
var svg = d3.select("#graph").append("svg")
.attr({
// "width" : width + margin.hor,
// "height" : height + margin.ver
"viewBox": "0 0 " + (width + margin.hor) + " " + (height + margin.ver),
"preserveAspectRatio": "xMinYMid"
})
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Load and treat data
// ===================
d3.csv(dataset, function(error, data) {
// Process data
// ============
data.forEach(function(d) {
// Parse date values
d[dateValue] = parseDate(d[dateValue]);
});
// Date range, ie x.domain()
// =========================
x.domain([
parseDate(lastElectionDate) || d3.min(data, function(d) { return d[dateValue]; }),
parseDate(nextElectionDate) || d3.max(data, function(d) { return d[dateValue]; })
]);
data = data.filter(function(d) {
// Filter data by date range
return x.domain()[0] <= d[dateValue] && d[dateValue] <= x.domain()[1]; })
.sort(function(a, b) {
// Non-descending date chronology breaks loess(),
// so we make sure to order the data properly.
return a[dateValue] - b[dateValue];
});
periods.forEach(function(d) {
d.start = parseDate(d.start);
d.end = parseDate(d.end);
d.start = d.start < x.domain()[0] ? x.domain()[0] : d.start;
d.end = d.end > x.domain()[1] ? x.domain()[1] : d.end;
});
// Domain (colours) for parties
// ============================
// Make a domain array of all headers,
// except those in ignoreFilter
color.domain(
d3.keys(data[0])
.filter(function(key) {
return ignoreFilter.indexOf(key) === -1;
}));
// Domain (colours) for polling firms
// ==================================
// Make a domain array of all polling institutes
instituteColor.domain(
// Group arrays of data by a key
d3.nest()
// Use the polling-firm column (`instituteValue`) as key
.key(function(d) { return d[instituteValue]; })
// Feed `data` to operation
.entries(data)
// Ignore the values and just return the keys (ie polling-firm names)
.map(function(d) { return d.key; })
);
// ! Redo as d3.nest
// Value Columns
// =============
var valueColumns =
// Get array of all parties
color.domain()
// And create a new object with party name as argument
.map(function(name) {
// Give the new object the following values
return {
// Party name (argument from color.domain())
"name" : name,
// Created nested array of date, value, and institute from data
"values" : data.map(function(d) {
return {
"date" : d[dateValue],
"dataValue" : +d[name],
"institute" : d[instituteValue]
};
})
};
});
// ! Redo after valueColumns is redone
// Domain for y
// ============
if (recalculateYMax === true && singleParty === true) {
y.domain([
0, d3.max(data, function(d) { return d[parties[0]]; })
]);
} else {
// ! Reconsider after implementing d3.nest
y.domain([
// Get the max value from valueColumns.values.dataValue
0, d3.max(valueColumns, function(c) { return d3.max(c.values, function(v) { return v.dataValue; }); })
]);
}
// X-axis
// ======
svg.append("g")
.attr({
"class" : "x axis",
"transform" : "translate(0," + height + ")"
})
.call(xAxis);
// Y-axis
// ======
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr({
"transform" : "rotate(-90)",
"y" : 6,
"dy" : ".71em"
})
.style("text-anchor", "end")
.text(yAxisTitle);
// Gridlines
// =========
// <https://www.d3noob.org/2013/01/adding-grid-lines-to-d3js-graph.html>
svg.append("g")
.attr({
"class" : "grid",
"transform" : "translate(0," + height + ")"
})
.call(xAxis
.tickSize(-height, 0, 0)
.tickFormat("")
.ticks(d3.time.months, 1)
);
// Period marker(s)
// ================
if (periods) {
svg.selectAll("rect")
.data(periods)
.enter().append("rect")
.attr({
"class" : "period",
"x" : function(d) { return x(d.start); },
"width" : function(d) { return x(d.end) - x(d.start); },
"y" : 0,
"height" : y(0),
"fill" : function(d) { return d.color || "steelBlue"; }
});
}
// Graph container
// ===============
var graph = svg.selectAll(".graph")
.data(valueColumns
.filter(function(d) {
return showAllParties === true ? d.name : parties.indexOf(d.name) !== -1;
}))
.enter().append("g")
.attr("class", "graph");
// Line displaying election date
// =============================
if (nextElectionDate) {
svg.append("line")
.attr({
"class" : "election-date-line",
"x1" : x.range()[1],
"x2" : x.range()[1],
"y1" : y.range()[0],
"y2" : y.range()[1]
});
}
// Line displaying voting threshold
// ================================
if (votingThreshold !== false) {
svg.append("line")
.attr({
"class" : "voting-threshold",
"x1" : x.range()[0],
"x2" : x.range()[1],
"y1" : y(votingThreshold),
"y2" : y(votingThreshold)
});
}
// LOESS regression plot
// =====================
// ! d[i][0]
if (singleParty === true) {
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return d[0]; }) // d[i][0]
.y(function(d) { return d[1]; }); // d[i][1]
graph.append("path")
.datum(function() {
var loess = science.stats.loess().bandwidth(.2);
data = data.filter(function(d) {
return !isNaN((d[parties[0]])); // d[parties[i]
});
// x(d[i][dateValue])
var xVal = data.map(function(d) { return x(d[dateValue]); }),
// d[parties[i]
yVal = data.map(function(d) { return y(d[parties[0]]); });
var loessData = loess(xVal, yVal);
return d3.zip(xVal, loessData);
})
.attr({
"class" : "line",
"d" : line
})
.style("stroke", function(d) {
return singleParty === true ? "#777" : color(d.name);
});
}
// Dots for each poll result
// =========================
if (showDots === true) {
var dots = svg.selectAll(".dot")
.data(valueColumns.filter(function(d) {
return showAllParties === true ? d.name : parties.indexOf(d.name) !== -1;
}))
.enter();
// for each party
// ! Optimize and refactor
var i = 0;
while (i < data.length) { // for each poll (in each party)
dots.append("circle")
.attr({
"class" : "dot",
"r" : 2,
"cx" : function(d) {
return !isNaN(y(d.values[i].dataValue)) ? x(d.values[i].date) : x.domain()[0]-1;
},
"cy" : function(d) {
return !isNaN(y(d.values[i].dataValue)) ? y(d.values[i].dataValue) : y.domain()[0]-1;
},
"stroke" : function(d) {
return displayInstitutes === false ? color(d.name) : instituteColor(d.values[i].institute);
}});
i += 1;
}
}
// Party legend
// ============
var legendSize = 18,
legendSpacing = legendSize + 2,
legendData = displayInstitutes === false ? color.domain() : instituteColor.domain();
var legend = svg.selectAll(".legend")
.data(legendData)
.enter().append("g")
.attr({
"class" : "legend",
"transform" : function(d, i) {
return "translate(" + 0 + "," + i * legendSpacing + ")"; }
});
legend.append("rect")
.attr({
"x" : width + legendSpacing,
"width" : legendSize,
"height" : legendSize
})
.style("fill", function(d) { return (displayInstitutes === false) ? color(d) : instituteColor(d); });
legend.append("text")
.attr({
"x" : width + legendSpacing + legendSize + 6,
"y" : legendSize / 2,
"dy" : ".35em"
})
.style("text-anchor", "start")
.text(function(d) { return d; });
// Period legend
// =============
if (periodLabels === true) {
var periodLegend = svg.selectAll(".period.legend")
.data(periods)
.enter().append("g")
.attr("transform", function() {
return "translate(" + 0 + "," + (legendData.length + 1) * legendSpacing + ")"; })
periodLegend.append("rect")
.attr({
"transform" : function(d, i) { // ! Include in parent translate instead
return "translate(" + 0 + "," + i * legendSpacing + ")"; },
"class" : "period",
"x" : width + legendSpacing,
"width" : legendSize,
"height" : legendSize
})
.style("fill", function(d) { return d.color || "steelBlue"; });
periodLegend.append("text")
.attr({
"transform" : function(d, i) { // ! Include in parent translate instead
return "translate(" + 0 + "," + i * legendSpacing + ")"; },
"x" : width + legendSpacing + legendSize + 6,
"y" : legendSize / 2,
"dy" : ".35em"
})
.style("text-anchor", "start")
.text(function(d) { return d.label; });
}
});
</script>
</body>
</html>
Modified http://d3js.org/d3.v3.min.js to a secure url
https://d3js.org/d3.v3.min.js