Public
Edited
Jan 30, 2024
1 star
Insert cell
Insert cell
chart = {
// Specify the chart’s dimensions.
const width = 928;
const height = width;
const radius = width / 6;
const padding = 50;

// Create the color scale.
const color = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, 11));

// Compute the layout.
const hierarchy = d3.hierarchy(data)
.sum(d => 1)
.sort((a, b) => b.value - a.value);
const root = d3.partition()
.size([2 * Math.PI, hierarchy.height + 1])
(hierarchy);
root.each(d => {d.current = d; d.data.name = d.data[0] || d.data.title
|| 'n/a'; d.value = d.data[1]
? d.data[1].length : 1});
console.log('heigharchy', hierarchy);

// Create the arc generator.
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius(d => d.y0 * radius)
.outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1))

// Create the SVG container.
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2 - padding/ 2, width, width + padding])
.style("font", "10px sans-serif");

// Append the arcs.
const path = svg.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.join("path")
// .attr("fill", d => { while (d.depth > 1) d = d.parent; return color(d.data.name); })
.attr("fill", d => {
if (d.data.provider) return brandColors[d.data.provider];
if (d.data.name == 'Movie' || d.data.name == 'Show') return color(d.data.name);
while (d.children) {
d = d.children[0];
}
if (d.data.provider) return brandColors[d.data.provider];
//while (d.depth > 1) d = d.parent;
return color(d.data.name);
})
.attr("fill-opacity", d => arcVisible(d.current) ? (d.children ? 0.6 : 0.4) : 0)
.attr("pointer-events", d => arcVisible(d.current) ? "auto" : "none")

.attr("d", d => arc(d.current));

// Make them clickable if they have children.
path.filter(d => d.children)
.style("cursor", "pointer")
.on("click", clicked);

const format = d3.format(",d");
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);

const label = svg.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.join("text")
.attr("dy", "0.35em")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.text(d => d.data.name);

const parent = svg.append("circle")
.datum(root)
.attr("r", radius)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("click", clicked);

// Handle zoom on click.
function clicked(event, p) {
parent.datum(p.parent || root);

root.each(d => d.target = {
x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
y0: Math.max(0, d.y0 - p.depth),
y1: Math.max(0, d.y1 - p.depth)
});

const t = svg.transition().duration(750);

// Transition the data on all arcs, even the ones that aren’t visible,
// so that if this transition is interrupted, entering arcs will start
// the next transition from the desired position.
path.transition(t)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => d.current = i(t);
})
.filter(function(d) {
return +this.getAttribute("fill-opacity") || arcVisible(d.target);
})
.attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0)
.attr("pointer-events", d => arcVisible(d.target) ? "auto" : "none")

.attrTween("d", d => () => arc(d.current));

label.filter(function(d) {
return +this.getAttribute("fill-opacity") || labelVisible(d.target);
}).transition(t)
.attr("fill-opacity", d => +labelVisible(d.target))
.attrTween("transform", d => () => labelTransform(d.current));
}
function arcVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0;
}

function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}

function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
const legend = createLegend(svg);
legend.attr("transform", `translate(${width / 2 - 120}, ${-height / 2 - padding / 2})`);

return svg.node();
}
Insert cell
streamchartBucketed = {
const data = videosByWeek.sort((a, b) => b.weekOfYear - a.weekOfYear);
// Specify the chart’s dimensions.
const width = 928;
const height = 500;
const marginTop = 20;
const marginRight = 10;
const marginBottom = 20;
const marginLeft = 40;
// debugger;
// Determine the series that need to be stacked.
const series = d3.stack()
.offset(d3.stackOffsetWiggle)
.order(d3.stackOrderInsideOut)
.keys(d3.union(data.map(d => d.provider))) // distinct series keys, in input order
.value(([, D], key) => D.get(key) ? D.get(key).numVideos : 0) // get value for each series key and stack
(d3.index(data, d => d.date, d => d.provider)); // group by stack then series key

// Prepare the scales for positional and color encodings.
const x = d3.scaleUtc()
.domain(d3.extent(data, d => d.date))
.range([marginLeft, width - marginRight]);

const y = d3.scaleLinear()
.domain(d3.extent(series.flat(2)))
.rangeRound([height - marginBottom, marginTop]);

const color = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(d3.schemeTableau10);

// Construct an area shape.
const area = d3.area()
.x(d => x(d.data[0]))
.y0(d => y(d[0]))
.y1(d => y(d[1]));

// Create the SVG container.
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");

// Add the y-axis, remove the domain line, add grid lines and a label.
svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y).ticks(height / 80).tickFormat((d) => Math.abs(d).toLocaleString("en-US")))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.style("font-family", "sans-serif")
.text("Watch time by provider"));

// Append the x-axis and remove the domain line.
svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).tickSizeOuter(0))
.call(g => g.select(".domain").remove());

// Append a path for each series.
svg.append("g")
.selectAll()
.data(series)
.join("path")
.attr("fill", d => brandColors[d.key])
.attr("d", area)
.append("title")
.text(d => d.key);

const legend = createLegend(svg);
legend.attr("transform", `translate(${width - marginRight - 120}, ${0})`);

// Return the chart with the color scale as a property (for the legend).
return Object.assign(svg.node(), {scales: {color}});
}
Insert cell
brandColors = {
return {
Netflix: 'rgb(229 9 21)',
Hulu: 'rgb(28 232 132)',
'Amazon': 'rgb(28 152 255)'
}
};
Insert cell
function createLegend(svg) {
const domains = Object.keys(brandColors);
// const svg = d3.create("svg")
// .attr("width", 400)
// .attr("height", 400)
// Legend as a group
const legend = svg.append("g")
// Apply a translation to the entire group
// .attr("transform", "translate(100, 100)")
const size = 20;
const border_padding = 15;
const item_padding = 5;
const text_offset = 2;
// Border
legend
.append('rect')
.attr("width", 120)
.attr("height", 125)
.style("fill", "none")
// .style("stroke-width", 1)
// .style("stroke", "black");
// Boxes
legend.selectAll("boxes")
.data(domains)
.enter()
.append("rect")
.attr("x", border_padding)
.attr("y", (d, i) => border_padding + (i * (size + item_padding)))
.attr("width", size)
.attr("height", size)
.style("fill", (d) => brandColors[d]);
// Labels
legend.selectAll("labels")
.data(domains)
.enter()
.append("text")
.attr("x", border_padding + size + item_padding)
.attr("y", (d, i) => border_padding + i * (size + item_padding) + (size / 2) + text_offset)
// .style("fill", (d) => color(d))
.text((d) => d)
.attr("text-anchor", "left")
.style("alignment-baseline", "middle")
.style("font-family", "sans-serif");
return legend;
}
Insert cell
videosByWeek = d3.rollups(videos, v => {
console.log(v);
// return v.length;
return v.reduce((total, v) =>
total + (v.isMovie ? 100 : 40)
, 0);
},
v => Math.floor((v.date.getMonth() * 30 + v.date.getDate()) / 7),
// v => v.date,
v => v.provider)
.map(d => {
const weekOfYear = d[0];
return {
weekOfYear,
date: new Date(new Date(2023, 0, 1).getTime() + weekOfYear * 7 * 24 * 60 * 60 * 1000),
videosPerProvider: d[1]
};
})
.flatMap(d =>
d.videosPerProvider.map(p => {
return {
date: d.date,
weekOfYear: d.weekOfYear,
provider: p[0],
numVideos: p[1]
}
}))
Insert cell
videosWithWeekOfYear = videos.map(v => {
const weekOfYear = Math.floor((v.date.getMonth() * 30 + v.date.getDate()) / 7);
return {
weekOfYear,
weekDate: new Date(new Date(2023, 0, 1).getTime() + weekOfYear * 7 * 24 * 60 * 60 * 1000),
...v
};
});
Insert cell
stats = {
}
Insert cell
data = dataMoviesFlattened
Insert cell
dataMoviesFlattened = {
const raw = d3.group(videosBySeries, g => g[1][0].isMovie && !g[1][0].isShow ? "Movie" : "Show");
console.log(JSON.stringify(raw), raw.get('Movie')[0][1])
// remove the N/A "series"
raw.set('Movie', raw.get('Movie').flatMap(m => m[1]));
console.log(raw);
console.log("videos", videos);
return raw;
}

Insert cell
videosBySeries = d3.group(videos, v => v.series || 'N/A')

Insert cell
videos = Object.entries(videosByProvider).flatMap(e => e[1]
.map(v => {
return {
...v,
date: new Date(Date.parse(v.date)),
provider: e[0]
};
})
.filter(v => v.date.getFullYear() >= 2023)
);

Insert cell
videosByProvider = FileAttachment("allstreaming@10.json").json()
Insert cell
// console.log(videosHulu)

Insert cell
videosHulu = dataCsv.map((row) => {
return {
title: row['Episode Name'],
season: row['Season'],
date: new Date(Date.parse(row['Last Played At'])),
series: row['Series Name'],
isMovie: row['Season'] == 'N/A',
}
})
.filter(v => !/Trailer/g.test(v.title))
.filter(v => isAllowed(v.title) || isAllowed(v.series))

Insert cell
dataCsv= FileAttachment("hulu.csv").csv()
Insert cell
isAllowed = function(text) {
for (let d of allowedTitles) {
if (new RegExp(d).test(text)) {
return true;
}
}
return false;
}

Insert cell
allowedTitles = [
"Culprits",
"Percy Jackson and the Olympians",
"Primeval",
"Such Brave Girls",
"Abbott Elementary",
"Reservation Dogs",
"A Murder at the End of the World",
"Class of '09",
"For the People",
"Schitt's Creek",
"Moonlighting",
"The Other Black Girl",
"SurrealEstate",
"Alaska Daily",
"Superstore",
"The Orville",
"Animaniacs",
"What We Do in the Shadows",
"The Mick",
"The Unit",
"Little Demon",
"Just Shoot Me",
"Fleishman Is in Trouble",
"Extraordinary",
"History of the World, Part II",
"Party Down",
"Will Trent",
"Speechless",
"Animal Control",
"The Fresh Prince of Bel-Air",
"Not Dead Yet",
"Accused",
"Rick and Morty",
"Burden of Truth",
"Kindred",
"Body of Proof",
"Lost",
"The Wonder Years",
"The Rookie",
"Timeless",

"The Retirement Plan",
"The Retirement Plan",
"Quiz Lady",
"Robots",
"Glengarry Glen Ross",
"Rosaline",
"Edge of Tomorrow",
"Jumanji: Welcome to the Jungle"
]

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