Poll of Polls for 2025 Election
Author
Compass Polling
Published
1:53PM 31 January 2025
Grey window can be dragged, made wider or more narrow to change the date range.
Blue line is the estimated average path, with the shaded area showing the 95% confidence range.
Roll over plot to reveal details of trajectory, individual polls and events.
Orange horizontal reference line indicates 2022 election result.
viewof theParty = Inputs.select(["ALP_2PP", "ALP", "Coalition", "Coalition_2PP", "GRN"], {label: "Select outcome to display", value: "Coalition_2PP"})data = Object.assign(
d3.csvParse(
await FileAttachment("xi.csv").text(), d3.autoType)
.filter(d => d.party == theParty)
.map(({date, xi, lo, up}) => ({date, value: xi, lo, up })),
{y: "↑ " + theParty }
)
/* xi = transpose(xi_raw)
data = Object.assign(
transpose(xi_raw).filter(d => d.party == theParty)
.map(({date, xi, lo, up}) => ({date, value: xi, lo, up })),
{y: "↑" + theParty }
)
*/
//datapolls_overlay = d3.csvParse(
await FileAttachment("polls_for_overlay.csv").text(), d3.autoType)
.filter(d => d.party == theParty)
.filter(d => !isNaN(d.y));
/*
polls_overlay = transpose(polls_for_overlay_raw)
.filter(d => d.party == theParty)
.filter(d => !isNaN(d.y));
*/
//polls_overlayevents = d3.csvParse(
await FileAttachment("events.csv").text(), d3.autoType);
//events
election_2022 = transpose(election_2022_raw);chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block")
.style("pointer-events","auto");
const clipId = DOM.uid("clip");
var plotting_region = svg.append("clipPath")
.attr("id", clipId.id)
.append("rect")
.attr("x", margin.left)
.attr("y", 0)
.attr("height", height)
.attr("width", width - margin.left - margin.right);
const gx = svg.append("g");
const gy = svg.append("g");
const ci_path = svg.append("path")
.datum(data)
.attr("clip-path", clipId)
.attr("fill","#CCCCCC7F");
console.log(ci_path);
const path = svg.append("path")
.datum(data)
.attr("clip-path", clipId)
.attr("fill","transparent")
.attr("stroke","blue")
.attr("stroke-width",4);
const d_overlay = svg.append("g")
.attr("clip-path", clipId)
.selectAll("circle")
.data(polls_overlay)
.join("circle")
.attr("cy", d => y(d.y))
.attr("cx", d => x(d.date))
.attr("r", d => z(d.Samplesize))
.attr("r_orig", d => z(d.Samplesize))
.attr("stroke", "#333")
.attr("fill", "transparent")
.attr("class","poll_circle")
.attr("id", d => "poll_circle_" + d.poll_id);
var crosshair = svg
.append("g")
.attr("clip-path", clipId)
.attr("class","crosshair")
.style("display","none");
var crosshair_horizontal = crosshair.append("line")
.attr("class","crosshair_line")
.attr("x1", margin.left)
.attr("x2", width - margin.right)
.attr("y1", 0)
.attr("y2", 0);
// Append vertical line
var crosshair_vertical = crosshair.append("line")
.attr("class", "crosshair_line")
.attr("x1", 0)
.attr("x2", 0)
.attr("y1", 0)
.attr("y2", height - margin.bottom);
// Append a circle at the intersection
var crosshair_circle = crosshair.append("circle")
.attr("class", "crosshair_circle")
.attr("r", 5);
// add info top right
var info_text = svg.append("text")
.attr("class", "info_text")
.attr("x",width - margin.right)
.attr("y",margin.top)
.attr("text-anchor","end")
.text("Roll over plot to reveal polls and trend");
// horizontal reference
var y2022 = election_2022.filter(d => d.party == theParty).map(d => d.result)[0];
var result_2022 = svg.append("line")
.attr("x1",margin.left)
.attr("x2",width - margin.right)
.attr("y1",y(y2022))
.attr("y2",y(y2022))
.attr("stroke","orange")
.attr("stroke-width",2);
var result_2022_text = svg.append("text")
.attr("class", "info_text")
.attr("x",margin.left)
.attr("y",y(y2022))
.attr("dy",-6)
.attr("text-anchor","start")
.text("2022 election result");
var events_line = svg.append("g")
.attr("clip-path", clipId)
.selectAll("line")
.data(events)
.join("line")
.attr("y1", height - margin.bottom)
.attr("y2", height - margin.bottom - 12)
.attr("x1", d => x(d.date))
.attr("x2", d => x(d.date))
.attr("stroke", "#ddd")
.attr("stroke-width",1)
.attr("id", d => "event_line_id_" + d.event_id);
var events_text = svg.append("g")
.attr("class", "info_text")
.attr("clip-path", clipId)
.selectAll("text")
.data(events)
.join("text")
.attr("y", height - margin.bottom)
.attr("x", d => x(d.date))
.attr("dx",-2)
.attr("dy",-2)
.attr("text-anchor","end")
.text(d => d.event)
.style("display","none")
.attr("id", d => "event_text_id_" + d.event_id);
// poll id text
var poll_text = svg.append("text")
.attr("class","info_text")
.attr("x",width - margin.right)
.attr("y",margin.top + 22)
.attr("text-anchor","end");
return Object.assign(svg.node(), {
update(focusX, focusY) {
gx.call(xAxis, focusX, height);
gy.call(yAxis, focusY, data.y);
events_line.attr("x1", d => focusX(d.date)).attr("x2", d => focusX(d.date));
ci_path.attr("d", area(focusX, focusY));
path.attr("d", line(focusX, focusY));
d_overlay
.attr("cx", d => focusX(d.date))
.attr("cy", d => focusY(d.y));
result_2022.attr("y1",focusY(y2022)).attr("y2",focusY(y2022));
result_2022_text.attr("y",focusY(y2022));
var selectedCircle_id = null;
var currentCircle_id = null;
var selectedEvent_id = null;
var currentEvent_id = null;
// Delaunay/Voronoi for mouseover etc
const delaunay = d3.Delaunay
.from(
polls_overlay,
d => focusX(d.date),
d => focusY(d.y)
);
const radius = 25;
// ineractions (many)
svg
.on("mousemove",
function(event,d){
let [mx, my] = d3.pointer(event);
//console.log(mouseX);
var nearestDataPoint = bisect(data,focusX.invert(mx));
var nearestEvent = bisect(events,focusX.invert(mx));
//console.log(nearestEvent);
//console.log(nearestDataPoint);
//console.log(nearestDataPoint.date);
//console.log(nearestDataPoint.value);
var xloc = focusX(nearestDataPoint.date);
var yloc = focusY(nearestDataPoint.value);
// Update line/circle position
crosshair_horizontal
.attr("x2",xloc)
.attr("y1",yloc)
.attr("y2",yloc);
crosshair_vertical
.attr("y1",yloc)
.attr("x1",xloc)
.attr("x2",xloc);
crosshair_circle
.attr("cx", xloc)
.attr("cy", yloc);
// Update text
info_text
.text(
[ "Poll average: " + d3.utcFormat("%b %e %Y")(nearestDataPoint.date),
theParty,
d3.format(".1%")(nearestDataPoint.value),
"±" + d3.format(".1f")(100*(nearestDataPoint.up - nearestDataPoint.lo)/2)
].join(" ")
);
// update circles
var allCircles = d3.selectAll(".poll_circle");
const p = delaunay.find(mx, my);
const d_test = polls_overlay[p];
const dist = Math.hypot(
mx - focusX(d_test.date),
my - focusY(d_test.y)
);
if(dist < radius){
currentCircle_id = d_test.poll_id;
if(currentCircle_id != selectedCircle_id){
allCircles
.interrupt()
.attr("fill","transparent")
.attr("r", function(){return this.getAttribute("r_orig");});
var thisCircle = d3.select("#" + "poll_circle_" + currentCircle_id);
var circleHighlight = thisCircle
.transition()
.duration(150) // Adjust the duration as needed
.attr("r",20)
//.on("end",function(){
// thisCircle
// .transition()
// .duration(250)
//.attr("r", function(){return this.getAttribute("r_orig");})
.on("end",function(){
thisCircle
.transition()
.duration(0)
.attr("fill","#FFA500A0")
//});
});
// update poll text
poll_text
.attr("display",null)
.datum(
[
"Highlighted poll:",
d_test.Brand,
d_test.Date,
theParty + ": " + d3.format(".1%")(d_test.y),
"Sample size: " + d3.format(",")(d_test.Samplesize)
].join("\n"))
.call(multilineText);
// update selected circle
selectedCircle_id = currentCircle_id;
}
} else {
d3.selectAll(".poll_circle")
.interrupt()
.attr("fill","transparent")
.attr("r", function(){return this.getAttribute("r_orig");});
poll_text.attr("display","none");
}
// update event
const event_dist = Math.abs(mx - focusX(nearestEvent.date));
if(event_dist < 20){
currentEvent_id = nearestEvent.event_id;
if(currentEvent_id != selectedEvent_id){
events_line
.attr("stroke-width",1)
.attr("stroke","#ddd")
.attr("y2",height - margin.bottom - 12);
events_text.style("display","none");
var thisEvent_line = d3.select("#" + "event_line_id_" + currentEvent_id);
console.log(thisEvent_line);
thisEvent_line
.attr("y2",margin.top)
.attr("stroke-width",2)
.attr("stroke","orange");
var thisEvent_text = d3.select("#" + "event_text_id_" + currentEvent_id);
thisEvent_text
.attr("x",d => focusX(d.date))
.attr("dx", d => focusX(d.date) > (width/2) ? -4 : 4)
.attr("text-anchor", d => focusX(d.date) > (width/2) ? "end" : "start")
.style("display",null);
selectedEvent_id = currentEvent_id;
}
} else {
events_line
.attr("stroke-width",1)
.attr("stroke","#ddd")
.attr("y2",height - margin.bottom - 12);
events_text.style("display","none");
}
selectedEvent_id = null;
}
);
svg.on("mouseover", function(){
crosshair.style("display", null);
});
svg.on("mouseout", function(){
crosshair.style("display", "none");
});
}
});
}
viewof focus = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, focusHeight])
.style("display", "block");
const brush = d3.brushX()
.extent([[margin.left, 0.5], [width - margin.right, focusHeight - margin.bottom + 0.5]])
.on("brush", brushed)
.on("end", brushended);
const defaultSelection = [x(d3.utcYear.offset(x.domain()[1], -1)), x.range()[1]];
svg.append("g")
.call(xAxis, x, focusHeight);
svg.append("path")
.datum(data)
.attr("fill", "#ccc")
.attr("d", area(x, y.copy().range([focusHeight - margin.bottom, 4])));
svg.append("path")
.datum(data)
.attr("fill", "transparent")
.attr("stroke","blue")
.attr("d", line(x, y.copy().range([focusHeight - margin.bottom, 4])));
const gb = svg.append("g")
.call(brush)
.call(brush.move, defaultSelection);
function brushed({selection}) {
if (selection) {
svg.property("value", selection.map(x.invert, x).map(d3.utcDay.round));
svg.dispatch("input");
}
}
function brushended({selection}) {
if (!selection) {
gb.call(brush.move, defaultSelection);
}
}
return svg.node();
}
line = (x, y) => d3.line()
.defined(d => !isNaN(d.value))
.x(d => x(d.date))
.y(d => y(d.value));
area = (x,y) => d3.area()
.defined(d => !isNaN(d.value))
.x(d => x(d.date))
.y0(d => y(d.lo))
.y1(d => y(d.up));
update = {
const [minX, maxX] = focus;
const maxY = d3.max([
d3.max(data, d => minX <= d.date && d.date <= maxX ? d.up : NaN),
d3.max(polls_overlay, d => minX <= d.date && d.date <= maxX ? d.y + .0025 : NaN)
]);
const minY = d3.min([
d3.min(data, d => minX <= d.date && d.date <= maxX ? d.lo : NaN),
d3.min(polls_overlay, d => minX <= d.date && d.date <= maxX ? d.y - .01 : NaN)
]);
chart.update(x.copy().domain(focus), y.copy().domain([minY, maxY]));
}
function multilineText(
el,
{
fontFamily,
fontSize = 11.5,
lineHeight = 1.45,
textAnchor = "end",
dominantBaseline = "auto"
} = {}
) {
el.each(function (text) {
const lines = text.split("\n");
const textContentHeight = (lines.length - 1) * lineHeight * fontSize;
const el = d3.select(this);
const anchor = {
x: +el.attr("x"),
y: +el.attr("y")
};
const dy =
dominantBaseline === "middle"
? -textContentHeight / 2
: dominantBaseline === "hanging"
? -textContentHeight
: 0;
el
.attr("font-family", fontFamily)
.attr("font-size", fontSize)
.attr("dominant-baseline", dominantBaseline)
.attr("text-anchor", textAnchor)
.selectAll("tspan")
.data(lines)
.join("tspan")
.text((d) => d)
.attr("x", anchor.x)
.attr("y", (d, i) => anchor.y + i * lineHeight * fontSize + dy);
});
}
x = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([margin.left, width - margin.right])
y = d3.scaleLinear()
.domain([
d3.min([
d3.min(polls_overlay, d => d.y) - .0025,
d3.min(data, d => d.lo)
]),
d3.max([
d3.max(polls_overlay, d => d.y) + .0025,
d3.max(data, d => d.up)
])
])
.range([height - margin.bottom, margin.top]);
z = d3.scaleLinear()
.domain(d3.extent(polls_overlay, d => d.Samplesize)).nice()
.range([3,10]);
xAxis = (g, x, height) => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0));
yAxis = (g, y, title) => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).tickFormat(d3.format(".0%")))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".title").data([title]).join("text")
.attr("class", "title")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(title));
bisect = {
const bisectDate = d3.bisector(d => d.date).center;
return (data, date) => data[bisectDate(data, date)];
}
margin = ({top: 20, right: 20, bottom: 30, left: 40})
height = 440;
focusHeight = 100;