Public
Edited
Nov 23, 2022
Insert cell
Insert cell
Insert cell
Insert cell
// load the csv file
csvDeathCounts = FileAttachment("filmdeathcounts.csv").csv()
Insert cell
// map the attributes
deathCountData = csvDeathCounts.map(d => ({
film: d['Film'],
year: +d['Year'],
bodyCount: +d['Body_Count'],
mpaaRating: d['MPAA_Rating'],
genre: d['Genre'].split("|"),
director: d['Director'],
length: +d['Length_Minutes'],
imdbRating: +d['IMDB_Rating']
}))
Insert cell
// checking the genre
genreList = {
const genres = []
for(let death of deathCountData){
death.genre.forEach(gen => {
// genres.push(gen)
if(!genres.includes(gen)){
genres.push(gen)
}
})
// genres.push(splitGenre)
}
return genres
}

Insert cell
deathCountData[0].genre.includes(genreList[0])
Insert cell
// counting movies based on genres
genresObj = {
let result = []
let eachGenre = {}
let genreCheckList = [];
for (let aGenre of genreList){
if(!genreCheckList.includes(aGenre)){
eachGenre[aGenre] = {"genre": aGenre, "total_film" : 0, "body_count": 0, "averageBody": 0}
genreCheckList.push(aGenre)
}
for(let death of deathCountData){
// result.push(death.genre.includes(aGenre))
if(death.genre.includes(aGenre)){
eachGenre[aGenre]["total_film"] += 1;
eachGenre[aGenre]["body_count"] += death.bodyCount;
}
}
eachGenre[aGenre]["averageBody"] = eachGenre[aGenre]["body_count"] / eachGenre[aGenre]["total_film"];
}

for(let each in eachGenre){
const currentobj = {
'genre': eachGenre[each]['genre'],
'total_film': eachGenre[each]['total_film'],
'body_count': eachGenre[each]['body_count'],
'averageBody': eachGenre[each]['body_count'] / eachGenre[each]['total_film']
};
result.push(currentobj);
}
// result.push(eachGenre)

return result
}

Insert cell
Insert cell
{
// use HTML to place the two charts next to each other
return html`
<div>
<div style="display: flex">${viewof yearOrDecade} ${viewof attributeSelection}</div>
<div> ${ShowBarchart}</div> <div>${ShowLineChartVis1}</div>
</div>`;
}
Insert cell
Insert cell
{
// use HTML to place the two charts next to each other
return html`
<div>
<div>
<div style='display:flex'>${viewof genreAttrib} <h2>Movie Death Trend by Genres</h2>
</div>
<div>${ShowGenreBarchart}</div>
</div>
<div> ${ShowScatterchart}</div>
</div>`;
}
Insert cell
d3.groupSort(genresObj, ([d]) => -d[genreAttrib], d => d.genre)
Insert cell
viewof genreAttrib = Inputs.select(["averageBody","body_count", "total_film"], {label: "Select attribute"})
Insert cell
Insert cell
Insert cell
Insert cell
mutable scatterPlotData = deathCountData.filter(death => death.genre.includes("War"))
Insert cell
d3.map(deathCountData, (d => d.length));
Insert cell
Insert cell
yearly_stats = {
const yearSummary = {};
const resultset = [];

for(let death of deathCountData){
const year = death.year;
if(year !== 1949 && year !== 2013){
if(!(year in yearSummary)){
yearSummary[year] = {'total_film' : 0, 'body_count': 0, 'total_length': 0, 'total_rating' : 0}
}
yearSummary[year]['total_film'] += 1;
yearSummary[year]['body_count'] += death.bodyCount;
yearSummary[year]['total_length'] += death.length;
yearSummary[year]['total_rating'] += death.imdbRating;
}
}
for(let year in yearSummary){
const currentobj = {
'year': +year,
'total_film': yearSummary[year]['total_film'],
'body_count': yearSummary[year]['body_count'],
'total_length': yearSummary[year]['total_length'],
'total_rating': yearSummary[year]['total_rating'],
'averageBody': yearSummary[year]['body_count'] / yearSummary[year]['total_film'],
'averageLength': yearSummary[year]['total_length'] / yearSummary[year]['total_film'],
'averageRating': yearSummary[year]['total_rating'] / yearSummary[year]['total_film'],
};
resultset.push(currentobj);
}

return resultset
}
Insert cell
d3.map(decade_stats, (d) => d.total_film)
Insert cell
decade_stats = {
const result = []
for(let year = 1951; year <= yearly_stats[yearly_stats.length-1].year; year+=10){
let decade = {'year': year, 'end': year+9, 'total_film': 0, 'body_count': 0, 'total_length' : 0, 'total_rating': 0}
for (let each of yearly_stats){
// result.push(each)
if(each.year >= year && each.year < year+10){ //1951 - 1960
decade.total_film += each.total_film;
decade.body_count += each.body_count;
decade.total_length += each.total_length;
decade.total_rating += each.total_rating;
}
}
decade.averageBody = decade.body_count / decade.total_film;
decade.averageLength = decade.total_length / decade.total_film;
decade.averageRating = decade.total_rating / decade.total_film;
result.push(decade);
}
return result
}
Insert cell
viewof attributeSelection = Inputs.select(["averageBody","body_count", "averageLength", "averageRating", "total_film"], {label: "Select Attribute"})
Insert cell
Insert cell
viewof yearOrDecade = Inputs.select(["year", "decade"], {label: "Select Year or Decade"})
Insert cell
Insert cell
titlesForTooltip = {
var titles = {'averageBody': 'body_count', 'body_count':'body_count', 'averageLength' : 'total_length', 'averageRating': 'total_rating', 'total_film': 'total_film'}

return titles
}
Insert cell
ShowBarchart = BarChart(chooseYearOrDecade, {
x: d => d.year,
y: d => d[attributeSelection],
xDomain: chooseYearOrDecade.map(d => d.year), // sort by descending frequency
yLabel: attributeSelection,
width: 1400,
height: 450,
color: "steelblue",
title1: 'year',
title2: attributeSelection,
title3: 'total_film',
title4: titlesForTooltip[attributeSelection]
})
Insert cell
ShowLineChartVis1 = LineChart(chooseYearOrDecade, {
x: d => d.year,
y: d => d[attributeSelection],
xDomain: d3.extent(chooseYearOrDecade, (d) => d.year),
yLabel: attributeSelection,
width: 1400,
height: 450,
color: "steelblue",
xType: d3.scaleLinear,
strokeWidth: 2
})
Insert cell
d3.extent(chooseYearOrDecade, (d) => d.year)
Insert cell
function LineChart(data, {
x = ([x]) => x, // given d in data, returns the (temporal) x-value
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
defined, // for gaps in data
curve = d3.curveLinear, // method of interpolation between points
marginTop = 20, // top margin, in pixels
marginRight = 30, // right margin, in pixels
marginBottom = 30, // bottom margin, in pixels
marginLeft = 40, // left margin, in pixels
width = 640, // outer width, in pixels
height = 400, // outer height, in pixels
xType = d3.scaleUtc, // the x-scale type
xDomain, // [xmin, xmax]
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // the y-scale type
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
color = "currentColor", // stroke color of line
strokeLinecap = "round", // stroke line cap of the line
strokeLinejoin = "round", // stroke line join of the line
strokeWidth = 1.5, // stroke width of line, in pixels
strokeOpacity = 1, // stroke opacity of line
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);
const I = d3.range(X.length);
if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y[i]);
const D = d3.map(data, defined);

// Compute default domains.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined) yDomain = [0, d3.max(Y)];

// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = yType(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).ticks(width / 40).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);

// Construct a line generator.
const line = d3.line()
.defined(i => D[i])
.curve(curve)
.x(i => xScale(X[i]))
.y(i => yScale(Y[i]));

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

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis);

svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.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")
.text(yLabel));

svg.append("path")
.attr("fill", "none")
.attr("stroke", color)
.attr("stroke-width", strokeWidth)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-opacity", strokeOpacity)
.attr("d", line(I));

return svg.node();
}
Insert cell
function BarChart(data, {
x = (d, i) => i, // given d in data, returns the (ordinal) x-value
y = d => d, // given d in data, returns the (quantitative) y-value
title, // given d in data, returns the title text
marginTop = 20, // the top margin, in pixels
marginRight = 0, // the right margin, in pixels
marginBottom = 30, // the bottom margin, in pixels
marginLeft = 40, // the left margin, in pixels
width = 640, // the outer width of the chart, in pixels
height = 400, // the outer height of the chart, in pixels
xDomain, // an array of (ordinal) x-values
xRange = [marginLeft, width - marginRight], // [left, right]
yType = d3.scaleLinear, // y-scale type
yDomain, // [ymin, ymax]
yRange = [height - marginBottom, marginTop], // [bottom, top]
xPadding = 0.1, // amount of x-range to reserve to separate bars
yFormat, // a format specifier string for the y-axis
yLabel, // a label for the y-axis
color = "currentColor", // bar fill color
title1,
title2,
title3,
title4
} = {}) {
// Compute values.
const X = d3.map(data, x);
const Y = d3.map(data, y);

// Compute default domains, and unique the x-domain.
if (xDomain === undefined) xDomain = X;
if (yDomain === undefined) yDomain = [0, d3.max(Y)];
xDomain = new d3.InternSet(xDomain);

// Omit any data not present in the x-domain.
const I = d3.range(X.length).filter(i => xDomain.has(X[i]));

// Construct scales, axes, and formats.
const xScale = d3.scaleBand(xDomain, xRange).padding(xPadding);
const yScale = yType(yDomain, yRange);
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
const yAxis = d3.axisLeft(yScale).ticks(height / 40, yFormat);

// Compute titles.
if (title === undefined) {
const formatValue = yScale.tickFormat(100, yFormat);
if(title4){
const film = d3.map(data, d => d[title3])
const totals = d3.map(data, d=> d[title4])
title = i => `${title1}: ${X[i]}
\n${title2}: ${formatValue(Y[i])}
\n${title3}: ${formatValue(film[i])}
\n${title4}: ${formatValue(totals[i])}`;
}else if(title3){
const film = d3.map(data, d => d[title3])
title = i => `${title1}: ${X[i]}
\n${title2}: ${formatValue(Y[i])}
\n${title3}: ${formatValue(film[i])}`;
}else{
title = i => `${title1}: ${X[i]}\n${title2}: ${formatValue(Y[i])}`;
}
} else {
const O = d3.map(data, d => d);
const T = title;
title = i => T(O[i], i, data);
}

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

svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(yAxis)
.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")
.text(yLabel));

const bar = svg.append("g")
.attr("fill", color)
.selectAll("rect")
.data(I)
.join("rect")
.attr("x", i => xScale(X[i]))
.attr("y", i => yScale(Y[i]))
.attr("height", i => yScale(0) - yScale(Y[i]))
.attr("width", xScale.bandwidth());

if (title) bar.append("title")
.text(title);

svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis);

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// define the margins
margin = ({top: 10, right: 20, bottom: 50, left: 105});
Insert cell
// define the width of the visualization
visWidth = 600;
Insert cell
// define the height of the visualization
visHeight = 500;
Insert cell
Insert cell
// encoding of MPAA ratings
mpaaRatings = Array.from(new Set(deathCountData.map(d => d.mpaaRating)));
Insert cell
// map colors to MPAA rating category using standard color scheme
mpaaColorCoding = d3.scaleOrdinal().domain(mpaaRatings).range(d3.schemeCategory10);
Insert cell
// define x using IMDB ratings
x = d3.scaleLinear()
.domain(d3.extent(deathCountData, d => d.imdbRating)).nice()
.range([0, visWidth])
Insert cell
// define y using body count
y = d3.scaleLinear()
.domain(d3.extent(deathCountData, d => d.bodyCount)).nice()
.range([visHeight, 0])
Insert cell
Insert cell
xAxis = (g, scale, label) =>
g.attr('transform', `translate(0, ${visHeight})`)
// add axis
.call(d3.axisBottom(scale))
// remove baseline
.call(g => g.select('.domain').remove())
// add grid lines
// references https://observablehq.com/@d3/connected-scatterplot
.call(g => g.selectAll('.tick line')
.clone()
.attr('stroke', '#d3d3d3')
.attr('y1', -visHeight)
.attr('y2', 0))
// add label
.append('text')
.attr('x', visWidth / 2)
.attr('y', 40)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.text(label)
Insert cell
yAxis = (g, scale, label) =>
// add axis
g.call(d3.axisLeft(scale))
// remove baseline
.call(g => g.select('.domain').remove())
// add grid lines
// refernces https://observablehq.com/@d3/connected-scatterplot
.call(g => g.selectAll('.tick line')
.clone()
.attr('stroke', '#d3d3d3')
.attr('x1', 0)
.attr('x2', visWidth))
// add label
.append('text')
.attr('x', -40)
.attr('y', visHeight / 2)
.attr('fill', 'black')
.attr('dominant-baseline', 'middle')
.text(label)
Insert cell
Insert cell
// scatterplot function
function scatterplot() {

// create the SVG
const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom)

// append the g element
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);

// define the x and y axes of the scatterplot
g.append("g").call(xAxis, x, 'IMDB Rating');
g.append("g").call(yAxis, y, 'Body Count');

// a tooltip for displaying film info
var tooltip = d3tip()
.attr("class", "tooltip")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("padding", "5px")

// apply tooltip to g
g.call(tooltip);

// draw points
g.selectAll('circle')

// specify which dataset is being used for plotting
.data(deathCountData)
.join('circle')
.attr('cx', d => x(d.imdbRating))
.attr('cy', d => y(d.bodyCount))
.attr('fill', d => mpaaColorCoding(d.mpaaRating))
.attr('opacity', 1)
.attr('r', 3);

// apply mouse hovering effects: mouseover
g.selectAll('circle').on('mouseover', function(d, i) {

// show info
tooltip.html(d => `
<div>
Film: ${i.film} </br>
Year: ${i.year} </br>
Body Count: ${i.bodyCount} </br>
IMDB Rating: ${i.imdbRating} </br>
MPAA Rating: ${i.mpaaRating}
</div>`)
tooltip.show(d, this)

// increase the opacity of the dot
d3.select(this).transition()
.duration('50')
.attr('opacity', '.50');

});

// apply mouse hovering effects: mouseout
g.selectAll('circle').on('mouseout', function(d, i) {

// make the dot solid
d3.select(this).transition()
.duration('50')
.attr('opacity', '1');

// do not show info
tooltip.hide(d, this);
});

// return the svg
return svg.node();
}
Insert cell
// call the scatterplot() function
scatterplot()
Insert cell
Insert cell
// brush function
function brush() {

// value for when there is no brush
const initialValue = deathCountData;

// create the svg
const svg = d3.create('svg')
.attr('width', visWidth + margin.left + margin.right)
.attr('height', visHeight + margin.top + margin.bottom)
.property('value', initialValue);

// append the g element
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);

// define the x and y axes titles
g.append("g").call(xAxis, x, 'IMDB Rating');
g.append("g").call(yAxis, y, 'Body Count');

// draw points
const dots = g.selectAll('circle')
.data(deathCountData)
.join('circle')
.attr('cx', d => x(d.imdbRating))
.attr('cy', d => y(d.bodyCount))
.attr('fill', d => mpaaColorCoding(d.mpaaRating))
.attr('opacity', 1)
.attr('r', 3);

// brush functionality
const brush = d3.brush()
// set the space that the brush can take up
.extent([[0, 0], [visWidth, visHeight]])
// handle events
.on('brush', onBrush)
.on('end', onEnd);

// append
g.append('g')
.call(brush);

// onBrush event handler
function onBrush(event) {

// use event.selection to get the coordinates of the top left
// and the bottom right of the brush box
const [[x1, y1], [x2, y2]] = event.selection;

// is the dot is in the brush box, return true; else, return false
function isBrushed(d) {
const cx = x(d.imdbRating);
const cy = y(d.bodyCount)
return cx >= x1 && cx <= x2 && cy >= y1 && cy <= y2;
}

// style the dots so the ones not selected are gray
dots.attr('fill', d => isBrushed(d) ? mpaaColorCoding(d.mpaaRating) : 'gray');

// update the data that appears in the variable for the vis
svg.property('value', deathCountData.filter(isBrushed)).dispatch('input');
}

// the onEnd event handler
function onEnd(event) {
// if the brush is cleared
if (event.selection === null) {
// reset the color of all of the dots
dots.attr('fill', d => mpaaColorCoding(d.mpaaRating));
svg.property('value', initialValue).dispatch('input');
}
}

// return the svg
return svg.node();
}
Insert cell
// display the array of selected dots
mpaaData
Insert cell
// create the scatter plot with brush functionailty; it is attached to the linked view below
viewof mpaaData = brush()
Insert cell
Insert cell
// counting movies based on mpaa ratings
mpaaRatingsCounts = {

// data structures
let result = []
let eachRating = {}
let ratingCheckList = [];

// loop through each rating
for (let aRating of mpaaRatings) {

// if the rating is not a part of the checklist
if (!ratingCheckList.includes(aRating)) {

// add it as a key of eachRating with a dictionary attached
eachRating[aRating] = {"mpaa_rating": aRating, "count" : 0}

// add to the ratings checlist
ratingCheckList.push(aRating)
}

// loop through each film selected by the brush
for (let aFilm of mpaaData) {

// if the current film has a rating of the current rating
if (aFilm.mpaaRating === aRating) {

// add to the count of its count key
eachRating[aRating]["count"] += 1;
}
}
}

// add to current object which will be pushed to the returned result array
for (let each in eachRating){
const currentobj = {
'mpaa_rating': eachRating[each]['mpaa_rating'],
'count': eachRating[each]['count']
};

// add to result array
result.push(currentobj);
}
// return the array containing mpaa ratings and how many of each there are
return result
}

Insert cell
// function for donut chart; based on the work of Mike Bostock
// https://observablehq.com/@d3/donut-chart
function donutChart(data, {

// given d in data, returns the (ordinal) label
name = ([x]) => x,

// given d in data, returns the (quantitative) value
value = ([, y]) => y,

// given d in data, returns the title text
title,

// outer width, in pixels
width = 640,

// outer height, in pixels
height = 400,

// inner radius of pie, in pixels (non-zero for donut)
innerRadius = Math.min(width, height) / 3,

// outer radius of pie, in pixels
outerRadius = Math.min(width, height) / 2,

// center radius of labels
labelRadius = (innerRadius + outerRadius) / 2,

// a format specifier for values (in the label)
format = ",",

// array of names (the domain of the color scale)
names,

// stroke separating widths
stroke = innerRadius > 0 ? "none" : "white",

// width of stroke separating wedges
strokeWidth = 1,

// line join of stroke separating wedges
strokeLinejoin = "round",

// angular separation between wedges
padAngle = stroke === "none" ? 1 / outerRadius : 0,
} = {}) {
// Compute values.
const N = d3.map(data, name);
const V = d3.map(data, value);
const I = d3.range(N.length).filter(i => !isNaN(V[i]));

// Unique the names.
if (names === undefined) names = N;
names = new d3.InternSet(names);

// Compute titles.
if (title === undefined) {
const formatValue = d3.format(format);

// variable to store total number of films represented
var totalFilms = 0;

// determine the total films represented
for (let aCount of data) {
totalFilms = totalFilms + aCount.count;
}

// text to display
title = i => `${N[i]}\n${formatValue(V[i])}\n${Math.round(((V[i] * 100) / totalFilms) * 100) / 100}%`;
} else {
const O = d3.map(data, d => d);
const T = title;
title = i => T(O[i], i, data);
}

// Construct arcs.
const arcs = d3.pie().padAngle(padAngle).sort(null).value(i => V[i])(I);
const arc = d3.arc().innerRadius(innerRadius).outerRadius(outerRadius);
const arcLabel = d3.arc().innerRadius(labelRadius).outerRadius(labelRadius);

// create svg
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");

// append g
svg.append("g")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-linejoin", strokeLinejoin)
.selectAll("path")
.data(arcs)
.join("path")
.attr("fill", d => mpaaColorCoding(N[d.data]))
.attr("d", arc)
.append("title")
.text(d => title(d.data))
// create legend inside of donut
var legendRectSize = 13;
var legendSpacing = 7;
var legend = svg.selectAll('.legend')
.data(mpaaColorCoding.domain())
.enter()
.append('g')
.attr('class', 'circle-legend')
.attr('transform', function (d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * mpaaColorCoding.domain().length / 2;
var horz = -2 * legendRectSize - 13;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});

// keys
legend.append('circle')
.style('fill', mpaaColorCoding)
.style('stroke', mpaaColorCoding)
.attr('cx', 0)
.attr('cy', 0)
.attr('r', '.5rem');

// labels
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function (d) {
return d;
});
return Object.assign(svg.node(), {scales: {mpaaColorCoding}});
}
Insert cell
// call donut function to see donut chart
donut()
Insert cell
// call donut chart function with mpaaRatingsCounts
function donut() {
const chart = donutChart(mpaaRatingsCounts, {
name: d => d.mpaa_rating,
value: d => d.count,
width,
height: 500
})
return chart;
}
Insert cell
Insert cell
// place the two views near each other
html`<h2>Body Count vs. IMDB Rating vs. MPAA Rating</h2><div style = "width: 70%;">${viewof mpaaData}${donut()}</div>`;
Insert cell
Insert cell
import { select } from "@jashkenas/inputs"
Insert cell
// import d3's library for tooltip
d3tip = require('d3-tip');
Insert cell
d3Legend = require('d3-svg-legend')
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