sentimentGrid = {
const styleText = `
h1, h2, h3, h4, h5, h6, p, table {
max-width: 900px;
font-family: sans-serif;
}
#SentimentGrid{
position:relative;
margin:auto;
font-family: sans-serif;
}
#SentimentGrid .states-grid{
position:absolute;
}
#SentimentGrid .state text{
fill:#6b6a69;
font-family: sans-serif;
text-anchor:middle;
font-size:12px;
}
#SentimentGrid p {
width:100%;
max-width: none;
margin:auto;
padding-bottom:10px;
margin: 20px 0px;
}
.pos { color:#f2bc27; font-weight:bold;}
.neg { color:#6eabcc; font-weight:bold;}
#SentimentGrid .annotation{
position:absolute;
text-anchor:middle;
font: 1em sans-serif;
}
#SentimentGrid .sentiment-detail{
overflow:visible;
}
#SentimentGrid .sentiment-detail text{
font-size:16px;
}
#SentimentGrid .controls{
width:400px;
margin:auto;
}
#SentimentGrid .controls .checkbox-container {
display:inline;
margin:auto 10px;
}
#SentimentGrid .controls .checkbox-container input{
margin:0px 10px
}
`
// (datasets array is defined at the bottom of the page)
// const data_by_state = d3.nest().key(d => d.state).entries(data)
const tweets_by_state = d3.nest().key(d => d.state).entries(datasets[0])
const retweets_by_state = d3.nest().key(d => d.state).entries(datasets[1])
const statenames = datasets[2]
statenames['ALL'] = 'United States'
// start with tweets only
var data_by_state = []
function set_data(type){
if (type == "tweets") {
data_by_state = tweets_by_state;
}
else if (type == "retweets") {
data_by_state = retweets_by_state;
}
}
set_data("retweets")
let margin = ({top: 10, bottom: 10, left: 10, right: 10});
const width = 900
let height = 600
const posfill = "#f2bc27" // "#45804e"
const negfill = "#6eabcc" // "#ae74b0"
const outerDiv = d3.create("div")
.attr("id", "SentimentGrid")
.style("width",width + margin.left + margin.right+"px")
.style("height",height + margin.top + margin.bottom+50+"px");
outerDiv.append("style").text(styleText)
// Controls
const controls = outerDiv.append("div").attr("class","controls")
const checkbox = controls.selectAll(".checkbox")
.data(["tweets","retweets"])
.enter().append("div").attr("class","checkbox-container")
.each(function(d,i){
d3.select(this).append("input").attr("type","radio").attr("name","sentiment-type").attr("value",d=>d)
.on("change", function(d){
drawChart(d3.select(this).attr("value"))
});
d3.select(this).append("label").html("show " + d)
});
controls.select('input[value="tweets"]').property("checked",true)
// SVG Container for the plot
const container = outerDiv.append("svg").attr("class","states-grid")
.attr("width",width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
let xGrid = d3.scaleLinear().domain([1,13]).range([0,width])
let yGrid = d3.scaleLinear().domain([1,8]).range([0,height])
var padding = 5
var w = xGrid(2)-2*padding,
h = yGrid(2)-2*padding;
function row(d) {
return d.values[0].row
}
function col(d) {
return d.values[0].col
}
function translate(d) {
return "translate(" + d.values[0].row + "," + d.values[0].col + ")"
}
// ------------------------------------------------------------------------------------------------DetailContaner
// Detail container in bottom right corner
// - shows closeup of state on hover
var detailPadding = 20,
detailUnits = 3
var detailW = xGrid(2)*detailUnits - 2*detailPadding,
detailH = yGrid(2)*detailUnits - 2*detailPadding;
const detail = outerDiv.append("svg").attr("class","sentiment-detail")
.attr("width",detailW+"px")
.attr("height", detailH+"px")
.attr("transform", "translate(" + (20+xGrid(2)*9) + "," + (yGrid(2)*4) + ")")
// background color box
detail.append('rect')
.attr("width",-10+xGrid(2)*detailUnits)
.attr('height',-10+yGrid(2)*detailUnits)
.attr('fill','white')
.attr('stroke','#adadad')
.attr("transform", "translate(5,5)").attr("rx",3).attr("ry",3)
// the plot layer
const detailGroup = detail.append("g").attr("class","detail-box").attr("transform", "translate(" + detailPadding + "," + detailPadding + ")")
// init with sentiment data for all the states
drawSentiment(detailGroup, get_state_data('ALL'), detailW, detailH)
detailGroup.select('.statename').text('United States').attr("text-anchor","middle")
// detail annotation layer
// date, numtweets, pos and neg words
var annotations = detail.append("g").attr("class","sentiment-annotation")
.attr("transform", "translate(" + detailPadding + "," + detailPadding + ")")
.attr("opacity", 0)
annotations.append("rect")
.attr("width", "100").attr("height", "50")
.attr("fill", "#000000").attr("rx",3).attr("ry",3)
.attr("x",detailPadding+padding-50+"px").attr("y", (detailH + detailPadding - 10-35)+"px")
var detailDate = annotations.append("text")
.text("March 1").attr("fill","#ffffff")
.attr("text-anchor","middle")
.attr("dy", (detailH + detailPadding - 20)+"px")
.attr("dx", detailPadding+padding+"px");
var detailNum = annotations.append("text")
.text("1,029 tweets")
.attr("text-anchor","middle").attr("fill","#ffffff")
.attr("dy", (detailH + detailPadding + 0)+"px")
.attr("dx", detailPadding+padding+"px");
function hideDetail(){
annotations.attr("opacity", 0)
detailGroup.select('.dateline').attr("opacity", '0')
}
function updateDetail(xloc, state_data){
detailGroup.select('.dateline').attr("opacity", '1')
.attr("transform", "translate("+xloc*detailW+","+0+")")
// set name to full name
detailGroup.select('.statename').text(statenames[state_data[0].state]).attr("text-anchor","middle")
var date = Math.min(1+ Math.floor(xloc*31), 31)
detailDate.text("March " + date)
// detailNum.text(Math.floor(Math.random()*2000) + " tweets").attr("opacity", 1)
var idx = Math.floor(date/2)
detailNum.text(state_data[idx].total + " tweets").attr("opacity", 1)
annotations.attr("opacity", 1).attr("transform", "translate("+xloc*detailW+","+detailPadding+")")
}
// ------------------------------------------------------------------------------------------------DetailContaner
//
// Draw each State
// wrapped this in a function so we can re-draw the grid on data update
// -----------------------------------------------------------------------------------------------------Draw Grid
function drawChart(type){
set_data(type)
container.selectAll(".state")// ".state"
.data(data_by_state)
//.enter().append("g")
.join("g")
.attr('width', w)
.attr('height', h)
.attr("class","state")
.attr("transform", function(d) {return "translate(" + xGrid(d.values[0].col-1) + "," + yGrid(d.values[0].row-1) + ")"})
.attr("state", d => d.key)
.each(function(d,i) {
// draw state data
drawSentiment(d3.select(this), get_state_data(d.key), w, h)
}).on("mouseover", function(d){
// show this state in the detail box
drawSentiment(detailGroup, get_state_data(d.key), detailW, detailH)
});
drawSentiment(detailGroup, get_state_data('ALL'), detailW, detailH)
}
// -----------------------------------------------------------------------------------------------------Draw Grid
drawChart("tweets")
function get_state_data(state){
return data_by_state.find(function(d) {return d.key == state}).values
}
function drawSentiment(svg, mydata, w, h) {
// clear the element
var currState = mydata[0].state;
svg.selectAll("*").remove()
// AXES
let xScale = d3.scaleLinear()
.domain([1,31])
.range([0,w])
let yScale = d3.scaleLinear()
.domain([-0.3,0.3])
.range([0,h])
// draw x-axis
if (svg.attr("class") == "detail-box"){
svg.append("g")
.attr("transform", "translate(0,"+h+")")
.call(d3.axisTop(xScale)
.ticks(2)
// .tickValues(xScale.domain().filter((d, i) => d % 5 === 0))
.tickValues([1,31])
.tickFormat(function (d) {
return "March " + d;
})
);
}
// draw the 0 (baseline)
// svg
// .append("line")
// .attr("x1", d => xScale(1)).attr("x2", d => xScale(31))
// .attr("y1", d => yScale(0)).attr("y2", d => yScale(0))
// .attr("stroke", "#bfbeba").attr("stroke-width", 1)
// draw positive regions
svg.append("path")
.datum(mydata)
.attr("fill", negfill).attr("stroke","none").attr("stroke-width", 1.5).attr("opacity",0.5)
.attr("d", d3.area()
// .defined(function(d) {return (d.pval <= 0.1);})
.x(function(d,i) {return xScale(d.date)})
.y0(function(d) {return yScale(0)})
.y1(function(d) {if (-d.sentiment > 0) {return yScale(-d.sentiment)} else {return yScale(0)} }));
// .y1(function(d) {if (-d.sentiment > 0 && d.pval <= 0.1) {return yScale(-d.sentiment)} else {return yScale(0)}}));
// draw negative regions
svg.append("path")
.datum(mydata)
.attr("fill", posfill).attr("stroke","none").attr("stroke-width", 1.5).attr("opacity",1)
.attr("d", d3.area()
// .defined(function(d) {return (d.pval <= 0.1);})
.x(function(d,i) {return xScale(d.date)})
.y0(function(d) {return yScale(0)})
.y1(function(d) {if (-d.sentiment < 0) {return yScale(-d.sentiment)} else {return yScale(0)} }));
// .y1(function(d) {if (-d.sentiment < 0 && d.pval <= 0.1) {return yScale(-d.sentiment)} else {return yScale(0)} }));
// mask out uncertain regions
// svg.append("path")
// .datum(mydata)
// .attr("fill", "#ffffff").attr("stroke","none").attr("stroke-width", 1.5).attr("opacity",0.5)
// .attr("d", d3.area()
// // .curve(d3.curveStep)
// .x(function(d,i) {return xScale(d.date)})
// .y0(function(d) {return yScale(0)})
// .y1(function(d) {if (d.pval > 0.1) {return yScale(-d.sentiment)} else {return yScale(0)} }));
// draw the line
svg
.append("path")
.datum(mydata)
.attr("fill", "none").attr("stroke-width", 1).attr("stroke","#000000")
.attr("d", d3.line()
// .defined(function(d) {return (d.pval <= 0.1);})
.x(function(d) { return xScale(d.date) })
.y(function(d) { return yScale(-d.sentiment) })
// .y(function(d) {if (d.pval <= 0.1) {return yScale(-d.sentiment)} else {return yScale(0)}})
// .y(function(d) {if (d.pval <= 0.1) {return yScale(-d.sentiment)} else {return null}}))
);
// draw uncertain line
// svg
// .append("path")
// .datum(mydata)
// .attr("fill", "none").attr("stroke-width", 1).attr("stroke","#000000").attr("opacity",0.5)
// .style("stroke-dasharray", ("3,3"))
// .attr("d", d3.line()
// .defined(function(d) {return (d.pval > 0.1);})
// .x(function(d) { return xScale(d.date) })
// // .y(function(d) { return yScale(-d.sentiment) })
// .y(function(d) { return yScale(0) })
// // .y(function(d) {if (d.pval <= 0.1) {return yScale(-d.sentiment)} else {return yScale(0)}})
// // .y(function(d) {if (d.pval <= 0.1) {return yScale(-d.sentiment)} else {return null}}))
// );
// .style("stroke", function(d) {
// if (d.pval > 0.1){return 'red'}
// else{ return 'black'}
// });
// state text label
svg.append("text")
.datum(mydata)
.attr("class","statename")
.attr("text-anchor", "middle")
.text(d => d[0].state == 'ALL' ? 'United States' : d[0].state)
.attr("dx", w/2) // use w/2 to center, 10 to left align
.attr("dy", "10")
// feedback vertical line
var feedback = svg.append("line").attr("class","dateline")
.attr("x1", d => 0).attr("x2", d => 0)
.attr("y1", d => yScale(-0.3)).attr("y2", d => yScale(0.3))
.attr("stroke", "#bfbeba").attr("stroke-width", 1)
.attr("opacity", "0")
// Add interactive layer
const mouseCatcher = svg.append("rect").attr("class", "mouse-catcher")
.attr("width", w).attr("height", h).attr("opacity", 0)
.on("mouseenter mousemove", function(d){
var coordinates= d3.mouse(this);
var x = coordinates[0];
var y = coordinates[1];
// console.log(x,y);
feedback.attr("opacity", "1").attr("transform", "translate("+x+","+0+")")
updateDetail(x/w, mydata)
}).on("mouseleave", function(d){
// hide off screen
feedback.attr("opacity", "0")
hideDetail()
});
}
return outerDiv.node()
}