timelinePlot = {
var nTopics = 10
var chartData = data
let margin = ({top: 30, bottom: 50, left: 40, right: 100});
const width = 750
let height = nTopics*(20+5)
const outerDiv = d3.create("div").attr("id", "CovidTopicsTimeline").style("width",width + margin.left + margin.right+"px");
outerDiv.append("p").text("In the month of March, Twitter users kept a close watch on COVID-19 updates in the news and discussed major concerns on the platform. Below is an interactive visualization that lets you explore which topics received the most attention on Twitter every day in March. Hover over a topic to track its popularity over time.");
const controls = outerDiv.append("div").attr("class","controls")
const showMore = controls.append("button").text("show more").attr("value", nTopics)
const showLess = controls.append("button").text("show less").attr("value", nTopics)
let accountFilter = controls.append('select').attr("id", "filter-accounts");
const allMessages = [ {display: "All accounts", value: "all"},
{display: "Humans", value: "human"},
{display: "Bots", value: "bot"}]
const items = accountFilter
.selectAll('options')
.data(allMessages)
.enter()
.append('option')
.text(function (d) { return d.display; })
.attr("value", function (d) { return d.value; })
items.filter(function(d) {return d.value === "All accounts"}).attr("selected",true);
// SVG Container for the plot
const container = outerDiv.append("svg").attr("width",width + margin.left + margin.right)
let curr_data = prepare_data(nTopics, chartData)
//
// Define X and Y Scales
//
let xScale = d3.scalePoint()
.domain(d3.keys(data[0]).slice(0,31).map(x => +x))
.range([0,width])
let yScale = d3.scalePoint()
.domain([...Array(nTopics).keys()])
.range([0,height])
// draw x-axis
container.append("g")
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisBottom(xScale)
.tickValues(xScale.domain().filter((d, i) => d % 5 === 0)));
//
// Initialize Vertical Grid Lines
//
const gridLines = container.append("g").attr("class", "vertical-grid")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("width", width).attr("height", height)
gridLines.append("line").attr("stroke", "#bfbeba").attr("stroke-width", 1).style("opacity",0)
//
// Color palette
//
var color = d3.scaleOrdinal()
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#dbaf2a','#a65628','#f781bf','#999999'])
//
// Initialize Topic Labels (they will animate to move around later)
//
const labelGroup = container.append("g").attr("class","topic-labels")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("height", height)
// Label Boxes
labelGroup.append("rect")//.attr("class", "labelContainer")
// Label Text
labelGroup.append("text").attr("dy", "15px").attr("dx", "5px")
//
// Initialize Timelines
//
const pathGroup = container.append("g").attr("class","topic-lines")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
pathGroup.append("path").attr("fill", "none").attr("stroke-width", 2).attr("class", "line")
//
// Initialize Hover Interaction
//
// Mouseover the grid - will monitor mousemove events
const mouse_catcher = container.append("g").attr("class", "mouse-catcher")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.append("rect").attr("width", width).attr("height", height).attr("opacity", 0)
// Date highlighter
var tickwidth = xScale(2) // width of one day
// create a group that will move left and right based on mouse position
const dateHelper = container.append("g").attr("class", "date-interactive")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
// add feedback rectangle and text
const dateFeedbackRect = dateHelper.append("rect")
.attr("width", tickwidth).attr("height", height+30)
.attr("fill", "#e8e8e8").attr("opacity", "0.5")
.style("pointer-events", "none")
// label box and text are nested in another group.
const dateLabel = dateHelper.append("g").attr("transform", "translate(0,"+(height+20)+")")
// add date label at the bottom
dateLabel.append("rect")
.attr("width", "100").attr("height", "20")
.attr("fill", "#000000").attr("rx",3).attr("ry",3).attr("x",(tickwidth-80)/2)
const dateLabelText = dateLabel.append("text").attr("class","dateText")
.attr("dy", "1.1em").attr("dx", "1em").text("March 12")
function follow_pointer(x) {
var w = xScale(2) // width of one day
var date = Math.floor(x/w);
// console.log(x,w,date, xScale(2))
if (date >= 0){
dateLabelText.text("March " + (date+1))
}
x = Math.floor(x/w)*(w)-(w/2);
dateHelper.attr("transform", () => "translate(" + (margin.left + x) + "," + (margin.top-10) + ")")
}
follow_pointer(-100)
mouse_catcher.on("mouseenter mousemove", function(d){
var coordinates= d3.mouse(this);
var x = coordinates[0];
var y = coordinates[1];
// console.log(x,y);
follow_pointer(x)
}).on("mouseleave", function(d){
// hide off screen
follow_pointer(-100)
});
//
// All the plot elements were initialized above
// The drawChart function updates all the elements with new data
//
function drawChart(nTopics){
//
// Adjust chart area
//
height = nTopics*(20+5)
container.attr("height", height + margin.top + margin.bottom)
//
// Prepare the data
//
let curr_data = prepare_data(nTopics, chartData)
//
// Update scales
//
yScale.domain([...Array(nTopics).keys()]).range([0,height])
//
// Color palette
//
var names = curr_data.map(function(d){ return d.key }) // list of group names
color.domain(names)
// Vertical Grid Lines
var w = xScale(2) // width of one day
gridLines.selectAll("line")
.data([...Array(32).keys()])
.join("line").attr("class", "grid")
.attr("x1", d => xScale(d)-(w/2)).attr("x2", d => xScale(d)-(w/2))
.attr("y1", -10).attr("y2", height+10)
.attr("stroke", "#bfbeba").attr("stroke-width", 1)
// Topic Label Boxes
let labels = labelGroup.selectAll("rect")
.data(curr_data)
.join("rect").attr("class", "labelContainer")
.attr("topic", d => d.selector)
.attr("x", width)
.attr("rank", d => d)
.attr("fill", function(d){ return color(d.key) })
// Topic Label Text
let labelsText = labelGroup.selectAll("text")
.data(curr_data)
.join("text").attr("class", "labelText").attr("dy", "15px").attr("dx", "5px")
.text(d => d.values[30].rank +" " +d.key)
// .attr("y", d => yScale(d.values[30].rank)-10)
.attr("x", width)
labels.transition()
.ease(d3.easeCubicInOut)
.duration(1000)
.attr("y", d => yScale(d.values[30].rank)-10)
labelsText.transition()
.ease(d3.easeCubicInOut)
.duration(1000)
.attr("y", d => yScale(d.values[30].rank)-10)
// draw lines
pathGroup.selectAll("path")
.data(curr_data)
.join("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 2)
.attr("class", "line")
.attr("topic", d => d.selector)
.attr("d", function(d){
return d3.line()
.curve(d3.curveMonotoneX)
// .curve(d3.curveCatmullRom.alpha(1))
.x(function(d) { return xScale(d.date) })
.y(function(d) { return yScale(d.rank) })
(d.values)
})
//
// Hover Interaction
// highlight topic label and line
//
labels.on("mouseover", function(d){
labelGroup.selectAll("rect").attr("class", "labelContainer background")
pathGroup.selectAll("path.line").attr("class", "line background")
this.setAttribute("class", "labelContainer")
pathGroup.select("path.line[topic="+this.getAttribute('topic')+"]").attr("class", "line")
}).on("mouseout", function(d){
labelGroup.selectAll("rect").attr("class", "labelContainer")
pathGroup.selectAll("path.line").attr("class", "line")
});
// Update Timeline Hover Interaction
mouse_catcher.attr("height", height)
dateFeedbackRect.attr("height", height+30)
dateLabel.attr("transform", "translate(0,"+(height+20)+")")
}
drawChart(nTopics)
///
// Show more/less topics
//
showMore.on("click", function(d) {
if (nTopics >= 50){
return
}
nTopics += 5
console.log("show more: " + nTopics)
drawChart(nTopics)
});
showLess.on("click", function(d) {
if (nTopics <= 5){
return
}
nTopics -= 5
console.log("show more: " + nTopics)
drawChart(nTopics)
});
///
// change data source
//
accountFilter.on('change', function() {
var selectedText = d3.select('#filter-accounts option:checked').text();
var selectedValue = d3.select("#filter-accounts").property("value") ;
var url = "https://raw.githubusercontent.com/mashabelyi/Twitter-Covid-Response/master/data/topics_timeline_v2_"+selectedValue+".csv";
d3.csv(url).then(function(loaded) {
chartData = loaded
console.log(chartData)
drawChart(nTopics)
});
})
// TRYING TO FIGURE OUT ANIMATION BELOW:
//
// function returning interpolated stroke-dasharray value
// function tweenDash() {
// // var l = this.node().getTotalLength()
// var i = d3.interpolateString("0," + l, l + "," + l);
// return function(t) { return i(t); };
// }
// Timeline animation
function set_date(date) {
// update lines
let paths = pathGroup.selectAll("path")
.join("path")
// do the animation; see the posts on arc animation for explanation
paths
// hide the arcs
.attr("stroke-dasharray", function () {
console.log(this.getTotalLength())
return this.getTotalLength()
})
.attr("stroke-dashoffset", function () {
return this.getTotalLength()
})
// reveal the arcs
.transition()
.duration(8000)
// .styleTween("stroke-dasharray", function(){
// var l = this.node().getTotalLength()
// var i = d3.interpolateString("0," + l, l + "," + l);
// return function(t) { return i(t); };
// })
.attr("stroke-dashoffset", 0)
// hide them again
// .transition()
// .attr("stroke-dashoffset", function () {
// return this.getTotalLength()
// })
// paths.transition()
// .ease(d3.easeCubicInOut)
// .duration(1000)
// .attr("d", function(d){
// return d3.line()
// .curve(d3.curveMonotoneX)
// // .curve(d3.curveCatmullRom.alpha(1))
// .x(function(d) { return xScale(d.date) })
// .y(function(d) { return yScale(d.rank) })
// (d.values.slice(0,date))
// })
}
// set_date(test_date)
return outerDiv.node();
}