Published unlisted
Edited
Mar 29, 2022
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Plot.lineY(snitchesWithFollowers, Plot.binX({y: "count"}, {x: "retweets", stroke:followedColorWithAds, thresholds: 10000})).plot({
x: {
type: "log",
base: 2,
},
y: {
label: 'Number of tweets in timeline'
// domain: [0, 100]
},
color: followedColorSettingsWithAds,
});
Insert cell
Insert cell
Insert cell
recommendedBreakdownTreemap = {
const recommendedBreakdownTreemap = Treemap({ children: recommendedRollup }, {
value: d => d[1], // size of each node (file); null for internal nodes (folders)
group: (d, n) => d[0],
label: (d, n) => d[0] + '\n' + Math.round(d[1] / snitchesFiltered.length * 100) + '%',
color: d => recommendationColorSettings.domain[0],
width: 720,
height: 720
});

d3.select(recommendedBreakdownTreemap).selectAll('rect')
.style('fill', d => {
// console.log('nod', d);
return recommendationColorScale(d.data[0]);
})
.style('fill-opacity', 1);
console.log('treemap: ', d3.select(recommendedBreakdownTreemap))
return recommendedBreakdownTreemap
}
Insert cell
Insert cell
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
tastemakers = Plot.plot({
...chartStyle,
height: 780,
marginLeft: 100,
// grid: true,
x: {
axis: "top",
round: true,
label: "Number of recommendations in feed",
// labelAnchor: "center",
// tickFormat: "+",
// transform: d => d * 100
},
y: {
label: null,
domain: d3.sort(snitchesByRecommendation, g => -(g[1].length)).map(d => d[0])
},
color: followedColorSettingsWithAds,
marks: [
Plot.barX(snitchesByRecommendation, {
y: g => g[0],
x: g => g[1].length,
fill: g => followedColorWithAds(g[1][0])
}),
Plot.ruleX([0])
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
snitchesAll = FileAttachment("twittysnitches-parsed@1.json").json()

Insert cell
snitches = snitchesAll.filter(d => !d.error).filter(d => d.tweetTextParsingConfidence > 0);
Insert cell
followers = FileAttachment("followers.json").json()

Insert cell
isFollower = authorHandle => followers.some(f => authorHandle && f.handle === "@" + authorHandle)
Insert cell
snitchesWithFollowers = snitches.map(s => {
const tweet = s.tweet.replaceAll('\\n', '\n');
//console.log(tweet);
return {
isFromFollowed: isFollower(s.authorHandle),
sentiment: vader.SentimentIntensityAnalyzer.polarity_scores(tweet),
tweet: tweet,
...s
}
});
Insert cell
snitchesFiltered = snitchesWithFollowers.map(d => {
const totalEngagement = d.retweets + d.likes + d.replies + 1;
return {
...d,
retweets: d.retweets + 1,
likes: d.likes + 1,
totalEngagement,
totalEngagementLog: Math.log10(totalEngagement)
};
})
.filter(d => d.totalEngagement > 0); // && d.totalEngagement < 50000
Insert cell
followedColor = d => d.isFromFollowed ? "Friend" : "Stranger";
Insert cell
followedColorSettings = {
return {
domain: ["Stranger", "Friend"],
range: ["rgb(78 121 167 / 50%)", "rgb(242 142 44 / 50%)"],
legend: true
}
};
Insert cell
adColor = 'rgb(214 65 161 / 50%)';
Insert cell
followedColorWithAds = d => d.isPromoted ? "Ad" : followedColor(d)
Insert cell
followedColorSettingsWithAds = {
const newSettings = {
...followedColorSettings
}
newSettings.domain = [...newSettings.domain, 'Ad'];
newSettings.range = [...newSettings.range, adColor];
console.log(JSON.stringify(newSettings))
return newSettings;
}
Insert cell
[...followedColorSettings.range, 'rgb(242 142 44 / 50%)']
Insert cell
d3.rollup(snitchesFiltered, ar => ar.length, followedColor)

Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
import {vader} from "@chrstnbwnkl/vader-sentiment-playground"
Insert cell
sentimentAnalyzer = new vader.SentimentIntensityAnalyzer()
Insert cell
d3.schemePastel1
Insert cell
recommendationLabels = (() => {
return {
numWhoLiked:'Liked by friend',
numWhoRetweeted:'Retweeted by friend',
numWhoReplied:'Reply from friend',
isReplyTo:'Reply to friend',
numWhoFollow:'Followed by friend',
}
})()
Insert cell
recommendedRollup = d3.sort(d3.flatRollup(snitchesFiltered, ar => ar.length, d => {
if (d.numWhoLiked > 0) {
return recommendationLabels.numWhoLiked;
} else if (d.numWhoRetweeted > 0) {
return recommendationLabels.numWhoRetweeted;
} else if (d.whoReplied.length > 0) {
return recommendationLabels.numWhoReplied;
} else if (d.isReplyTo) {
return recommendationLabels.isReplyTo;
} else if (d.numWhoFollow > 0) {
return recommendationLabels.numWhoFollow;
}

return followedColorWithAds(d);
}), ar => -ar[1])
Insert cell
recommendedRollupMap = d3.rollup(snitchesFiltered, ar => ar.length, d => {
if (d.numWhoLiked > 0) {
return recommendationLabels.numWhoLiked;
} else if (d.numWhoRetweeted > 0) {
return recommendationLabels.numWhoRetweeted;
} else if (d.whoReplied.length > 0) {
return recommendationLabels.numWhoReplied;
} else if (d.isReplyTo) {
return recommendationLabels.isReplyTo;
} else if (d.numWhoFollow > 0) {
return recommendationLabels.numWhoFollow;
}

return followedColorWithAds(d);
})
Insert cell
d3.union(followedColorSettingsWithAds.range, d3.schemePastel2)
Insert cell
recommendationColorSettings = {
const newSettings = {
...followedColorSettingsWithAds
}
newSettings.domain = [...newSettings.domain, ...Object.values(recommendationLabels)];
newSettings.range = Array.from(d3.union(newSettings.range, d3.schemePastel2.map(c => {
const newColor = d3.color(c);
newColor.opacity = 0.5;
return newColor.formatRgb();
}))).slice(0, newSettings.domain.length);
return newSettings;
}
Insert cell
recommendationColorScale = d3.scaleOrdinal().domain(recommendationColorSettings.domain).range(recommendationColorSettings.range)
Insert cell
recommendedBreakdownDonut = DonutChart(recommendedRollup, {
name: d => d[0],
value: d => (d[1] / snitchesFiltered.length),
format: '.0%',
names: recommendationColorSettings.domain,
colors: recommendationColorSettings.range,
width,
height: 500
})
Insert cell
import {Treemap} from "@d3/treemap"
Insert cell
recommendedBreakdownTreemapNested = {
const topLevelNames = ['Friend', 'Ad']
const topLevel = recommendedRollup.filter(d => topLevelNames.includes(d[0]));
const recommendedRollupStrangerChildren = recommendedRollup.filter(d => !topLevelNames.includes(d[0]));
const recommendedRollupNested = topLevel;
const strangers = ['Strangers', d3.sum(recommendedRollupStrangerChildren, d => d[1])];
strangers.children = recommendedRollupStrangerChildren
recommendedRollupNested.push(strangers);
const hierarchy = d3.hierarchy({children: recommendedRollupNested})
.sum(d => d.children ? 0 : d[1])
.sort(d => d[1]);
// console.log('hierarchy', hierarchy)
const recommendedBreakdownTreemap = Treemap(
hierarchy,
{
value: d => d.data[1], // size of each node (file); null for internal nodes (folders)
// group: (d, n) => d[0],
label: (d, n) => d.data[0] + '\n' + Math.round(d.data[1] / snitchesFiltered.length * 100) + '%',
color: d => recommendationColorSettings.domain[0],
width: 720,
height: 720
});

d3.select(recommendedBreakdownTreemap).selectAll('rect')
.style('fill', d => {
// console.log('nod', d);
return recommendationColorScale(d.data[0]);
})
.style('fill-opacity', 1);
// console.log('treemap: ', d3.select(recommendedBreakdownTreemap))
return recommendedBreakdownTreemap
}
Insert cell
snitchesFiltered.filter( d => d.numWhoLiked + d.numWhoRetweeted + d.whoReplied.length + (d.isReplyTo ? 1 : 0) === 0 && !d.isFromFollowed);
Insert cell
timeBucketScale = Plot.scale({
x: {
domain: d3.extent(snitchesFiltered.filter(d => d.postedMillisecondsAgo > 0), d => d.postedMillisecondsAgo),
// range: [0, 600],
type: 'log',
// type: "pow", exponent: 1/ 3,
ticks: 3,
tickFormat: ms => dayjs().subtract(ms).fromNow()
},
});
Insert cell
timeBucketThresholds = d3.range(timeBucketScale.domain[0] / 1000 / 60, timeBucketScale.domain[1] / 1000 / 60, 60 * 5).map(d => d * 1000 * 60)
// .map(d => Math.pow(d, 2))
//.map( timeBucketScale.apply).map(d => d * timeBucketScale.domain[1]) //.map(Math.log10)
Insert cell
timeBucketThresholds.map(ms => dayjs().subtract(ms).fromNow())
Insert cell
d3.scaleLog().domain(5, 120, 6*24*6)
Insert cell
snitchesByAuthorRollup = d3.flatRollup(snitchesFiltered, g => g.length, d => d.authorHandle).filter(g => g[1] > 1)
Insert cell
snitchesByAuthor = d3.sort(d3.groups(snitchesFiltered, d => d.authorHandle), g => -(g[1].length))
.filter(g => g[0] !== 'odbol')
.filter(g => g[1].length > 1)
.slice(0, 70)
Insert cell
snitchesByAuthorMap = d3.group(snitchesFiltered, d => d.authorHandle)
Insert cell
allFeaturedAuthorsFollowed = d3.groups(snitchesFiltered.filter(d => d.isFromFollowed), d => d.authorHandle)
Insert cell
followedColorSequentialScheme = d3.scaleSequentialLog(d3.interpolatePuOr).domain([200000, 1])
Insert cell
followedColorSequentialScheme(10000)

Insert cell
authorGridWColors = Plot.plot({
...chartStyle,
height: 780,
marginLeft: 100,
// grid: true,
x: {
axis: "top",
round: true,
label: "Number of tweets in feed",
// labelAnchor: "center",
// tickFormat: "+",
// transform: d => d * 100
},
y: {
label: null,
domain: d3.sort(snitchesByAuthor, g => -(g[1].length)).map(d => d[0])
},
// color: {
// // domain: [0, 10000],
// type: 'log',
// // scheme: "magma",
// scheme: 'YlGnBu',
// label: "Total engagement (Retweets + Likes + Replies)",
// legend: true
// },
marks: [
Plot.cell(snitchesByAuthor
.map(g => {
const tweetsIndexed = d3.sort(g[1], d => d.totalEngagement)
.map((d, index) => {
return {
index,
...d
};
});
return [g[0], tweetsIndexed];
})
.flatMap(g => g[1]), {
y: d => d.authorHandle,
x: d => d.index,
fill: d => followedColorSequentialScheme(d.totalEngagement),
title: d => `${d.index} ${d.retweets} + ${d.likes}`,
// padding:0,
// margin:0
}),
// Plot.ruleX([0])
// Plot.text(d3.sort(snitchesByAuthor, g => -(g[1].length)).map(d => d[0]), {
// x: "season",
// y: "authorHandle",
// text: d => d.authorHandle,
// title: "title"
// })
]
})
Insert cell
snitchesByRecommendation = d3.sort(d3.groups(snitchesFiltered, d => {
const whoRecommended = d.whoLiked.concat(d.whoRetweeted, d.whoReplied, d.followedBy)
.filter(d => !/\d+ others/.test(d))
.map(d => d.trim());
// console.log('who', whoRecommended);
if (!whoRecommended) return null;
return whoRecommended[0];
})
, g => -(g[1].length))
// remove undefined (i.e. tweets that weren't recommended)
.filter(g => g[0])
.map(g => {
// map author name to author handles
const author = followers.find(f => f.name.includes(g[0]) || g[0].includes(f.name));
return [author ? author.handle.substr(1) : g[0], g[1]];
})
// .filter(g => g[0] !== 'odbol')
.filter(g => g[1].length > 1)
.slice(0, 70)
Insert cell
chartStyle = (() => {
return {
width: 720,
height: 480,
fontFamily: 'sans-serif'
}
})()
Insert cell
dayjs = require('https://unpkg.com/dayjs@1.8.21/dayjs.min.js')
Insert cell
dayjsRelativeTime = require('https://unpkg.com/dayjs@1.8.21/plugin/relativeTime.js')
Insert cell
dayjs.extend(dayjsRelativeTime)
Insert cell
import {DonutChart} from "@d3/donut-chart"
Insert cell
donutLabel = (donutChart, labelText, labelTextLine2) => {
const label = d3.select(donutChart)
.append("text")
.text(labelText)
.attr('y', 15)
.attr('font-size', 48)
.attr('fill', '#555')
.attr('text-anchor', "middle")
.attr('font-family', 'sans-serif')
.attr("font-weight", 'bold');

if (labelTextLine2) {
label.attr('y', 0);
label.append('tspan')
.text(labelTextLine2)
.attr('x', 0)
.attr('dy', 36)
.attr('font-size', 32)
}
}
Insert cell
import {WordCloud, stopwords} from "@d3/word-cloud"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more