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

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