Published unlisted
Edited
Apr 13, 2021
Insert cell
Insert cell
Insert cell
d3.csvParse(await FileAttachment("testing_output_v8@1.csv").text(), d3.autoType)
Insert cell
data = {
const text = await FileAttachment("testing_output_v8@1.csv").text();
const parseDate1 = d3.utcParse(" %H:%M:%S.%f");
const parseDate2 = d3.utcParse("%Y-%m-%d %H:%M:%S"); // some dates do not have partial seconds,
const parseDateOuter = function(timestamp){ // this function handles both date parsers
var date1 = parseDate1(timestamp);
if (date1 == null){
return parseDate2(timestamp)
}
return date1;
}
return d3.csvParse(text, ({platform, timestamp, polarity, subjectivity, word_count, sent_or_received, other_person}) => ({
platform: platform,
date: parseDateOuter(timestamp),
polarity: polarity,
subjectivity: subjectivity,
word_count: word_count,
sent_or_received: sent_or_received,
contact: other_person
}));
}
Insert cell
// check for null dates
nullDates = data.filter( x =>
x.date == null );
Insert cell
// notebook is too slow with 68k data points. Downselect based on the most contacted people
// create dict with key:value => contact:number of messages
tally_messages = function(msgs)
{
var tally = {};
for (var i = 0; i < msgs.length; i++){
if (msgs[i].contact in tally){
tally[msgs[i].contact] = tally[msgs[i].contact] + 1;
} else{
tally[msgs[i].contact] = 1;
}
}
return tally;
}
Insert cell
// count up the number of exchanged messages
myTally = tally_messages(data)
// this might be an interesting bubble chart by itself

Insert cell
// turn into a sort-able array
to_array = function(m_dict){
var to_ret = []
for (var attrib in m_dict){
to_ret.push({"contact":attrib, "count":m_dict[attrib]})
}
return to_ret
}
Insert cell
myTallyArr = to_array(myTally)
Insert cell
// sort the array
myTallyArr.sort((b, a) => {
return a.count - b.count;
});
Insert cell
// might be interesting to visualize the distribution of message count across contacts
// How evenly distributed are my social interactions?
// let's make a histogram
hist_margin = ({top: 10, right: 30, bottom: 30, left: 40});
Insert cell
hist_width = 460 - margin.left - margin.right;
Insert cell
hist_height = 400 - margin.top - margin.bottom;
Insert cell
hist_var = "count"
Insert cell
bins = {
const bin = d3.bin().value(d => d.count).thresholds(100);
return bin(myTallyArr);
}
Insert cell
bins[9].x0
Insert cell
hist_x = d3.scaleLinear()
.domain([bins[0].x0, bins[bins.length - 1].x1])
.range([hist_margin.left, hist_width - hist_margin.right])
Insert cell
hist_y = d3.scaleLinear()
.domain([0, d3.max(bins, d => d.length)]).nice()
.range([hist_height - hist_margin.bottom, hist_margin.top])
Insert cell
hist_xAxis = g => g
.attr("transform", `translate(0,${hist_height - hist_margin.bottom})`)
.call(d3.axisBottom(hist_x).ticks(hist_width / 60 ).tickSizeOuter(0))
.call(g => g.append("text")
.attr("x", hist_width - hist_margin.right)
.attr("y", -4)
.attr("fill", "currentColor")
.attr("font-weight", "bold")
.attr("text-anchor", "end")
.text(data.x))
Insert cell
hist_yAxis = g => g
.attr("transform", `translate(${hist_margin.left},0)`)
.call(d3.axisLeft(hist_y).ticks(hist_height / 40))
.call(g => g.select(".domain").remove())
.call(g => g.select(".tick:last-of-type text").clone()
.attr("x", 4)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(data.y))
Insert cell
// I finally get it!! Everything is based off the data...
// simply changing the bins data structure changes literally everything about the viz... scales, axis, etc...
// so a function that modifies the bins data structure could use inputs from a control, making the viz controllable
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, hist_width, hist_height]);
svg.append("g")
.attr("fill", "steelblue")
.selectAll("rect")
.data(bins)
.join("rect")
.attr("x", d => hist_x(d.x0) + 1)
.attr("width", d => Math.max(0, hist_x(d.x1) - hist_x(d.x0) - 0.1))
.attr("y", d => hist_y(d.length))
.attr("height", d => hist_y(0) - hist_y(d.length));
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", hist_width-125)
.attr("y", hist_height - 4)
.attr("font-size", "9px")
.text("Quantity of Messages Exchanged");
svg.append("text")
.attr("class", "title")
.attr("text-anchor", "end")
.attr("x", hist_width-80)
.attr("y", 15)
.attr("font-size", "10px")
.text("Histogram of Total Message Count by Contact");
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", 2)
.attr("x", -120)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.attr("font-size", "9px")
.text("Count of Contacts");
svg.append("g")
.call(hist_xAxis);
svg.append("g")
.call(hist_yAxis);

return svg.node();
}
Insert cell
// Let's make a bubble chart to show the relative sizes of contacts
Insert cell
num_contacts = 150; // start with dunbar number
Insert cell
top_contacts = myTallyArr.slice(0, num_contacts)
Insert cell
// pack circles of the top contacts
Insert cell
// prep sizes
margin = ({top: 25, right: 20, bottom: 35, left: 40})
Insert cell
width = 750
Insert cell
height = 600
Insert cell
x_var = "date"
Insert cell
y_var = "word_count"
Insert cell
// start with something simple - maybe just an xy plot of word_count vs. time. That would be interesting
x_scale_time = d3.scaleTime()
.domain([d3.min(data, d => d[x_var]), d3.max(data, d => d[x_var])])
.range([margin.left, width - margin.right])
Insert cell
d3.min(data, d => d[x_var])
Insert cell
d3.max(data, d => d[x_var])
Insert cell
x_axis_time = g => g
.attr("transform", `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x_scale_time))
Insert cell
y_scale_word_count = d3.scaleLinear()
.domain([0, d3.max(data, d => d[y_var])])
.range([height - margin.bottom, margin.top])
Insert cell
y_axis_word_count = g => g
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y_scale_word_count))
Insert cell
// color scale for sentiment
sentiment_color_scale = d3.scaleLinear().domain([-1,1])
.range(["white", "blue"])
Insert cell
sentiment_color_scale(0)
Insert cell
// make the scatterplot
// make the svg canvas that we will bind the axis to

scatterplot = {
const svg = d3.create("svg")
.attr("viewBox", [0,0, width, height]);
svg.append("g")
.call(x_axis_time);
svg.append("g")
.call(y_axis_word_count);
svg.append("g")
.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => x_scale_time(d[x_var]))
.attr("cy", d => y_scale_word_count(d[y_var]))
.attr("r", 1)
.attr("opacity", .5);
//.attr("color", "rgb(0, 0, 255)")
//.attr("color", d => sentiment_color_scale(d.polarity));
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", width-350)
.attr("y", height - 4)
.attr("font-size", "12px")
.attr("font-family", "Helvetica")
.text("Date");
svg.append("text")
.attr("class", "title")
.attr("text-anchor", "end")
.attr("x", width-200)
.attr("y", 15)
.attr("font-size", "14px")
.attr("font-family", "Helvetica")
.text("Scatterplot of Message Word Count and Date");
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", 2)
.attr("x", -200)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.attr("font-size", "12px")
.attr("font-family", "Helvetica")
.text("Message Word Count");
return svg.node(); // returns handle to svg
}
Insert cell
chart_weekly = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("overflow", "visible");

svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);

const path = svg.append("g")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.selectAll("path")
.data(data2.series)
.join("path")
.style("mix-blend-mode", "multiply")
.attr("d", d => line(d.values));

svg.call(hover, path);

return svg.node();
}
Insert cell
data2 = {
const data2 = d3.csvParse(await FileAttachment("weekly_time_series@3.csv").text());
const columns = data2.columns.slice(1);
return {
y: "Weekly Message Exchanges",
series: data2.map(d => ({
name: d.other_person,
values: columns.map(k => +d[k])
})),
dates: columns.map(d3.utcParse("%Y-%m-%d"))
};
}
Insert cell
function hover(svg, path) {
if ("ontouchstart" in document) svg
.style("-webkit-tap-highlight-color", "transparent")
.on("touchmove", moved)
.on("touchstart", entered)
.on("touchend", left)
else svg
.on("mousemove", moved)
.on("mouseenter", entered)
.on("mouseleave", left);

const dot = svg.append("g")
.attr("display", "none");

dot.append("circle")
.attr("r", 2.5);

dot.append("text")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("y", -8);

function moved(event) {
event.preventDefault();
const pointer = d3.pointer(event, this);
const xm = x.invert(pointer[0]);
const ym = y.invert(pointer[1]);
const i = d3.bisectCenter(data2.dates, xm);
const s = d3.least(data2.series, d => Math.abs(d.values[i] - ym));
path.attr("stroke", d => d === s ? null : "#ddd").filter(d => d === s).raise();
dot.attr("transform", `translate(${x(data2.dates[i])},${y(s.values[i])})`);
dot.select("text").text(s.name);
}

function entered() {
path.style("mix-blend-mode", null).attr("stroke", "#ddd");
dot.attr("display", null);
}

function left() {
path.style("mix-blend-mode", "multiply").attr("stroke", null);
dot.attr("display", "none");
}
}
Insert cell
height_m = 600
Insert cell
margin_m = ({top: 20, right: 20, bottom: 30, left: 30})
Insert cell
x = d3.scaleUtc()
.domain(d3.extent(data2.dates))
.range([margin_m.left, width - margin_m.right])
Insert cell
y = d3.scaleLinear()
.domain([0, d3.max(data2.series, d => d3.max(d.values))]).nice()
.range([height_m - margin_m.bottom, margin_m.top])
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height_m - margin_m.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0))
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin_m.left},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.select(".tick:last-of-type text").clone()
.attr("x", 3)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(data2.y))
Insert cell
line = d3.line()
.defined(d => !isNaN(d))
.x((d, i) => x(data2.dates[i]))
.y(d => y(d))
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more