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 = true,
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