Public
Edited
May 1, 2023
Insert cell
Insert cell
Insert cell
chart = {

// Initialize svg
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

// var svg = d3.select(DOM.svg(width, height));
var domainwidth = width - margin.left - margin.right,
domainheight = height - margin.top - margin.bottom;
var x = d3.scaleLinear()
.domain([1,6])
.range([0, domainwidth]);
var y = d3.scaleLinear()
.domain([1,6])
.range([domainheight, 0]);
var g = svg.append("g")
.attr("transform", "translate(" + margin.top + "," + margin.top + ")");
g.append("rect")
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom)
.attr("fill", "#F6F6F6");
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + y.range()[0] / 2 + ")"); // `translate(0, ${y.range()[0]} / 2)` ???
// .call(d3.axisBottom(x).ticks(6));

g.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + x.range()[1] / 2 + ", 0)");
// .call(d3.axisLeft(y).ticks(6));

var br = g.append("svg")
.attr('class', 'seg')
.attr("x", x(3.5))
.attr("y", y(3.5))
br.append("rect")
.attr("width", x(6) - x(3.5))
.attr("height", y(3.5))
.attr("fill", "lightblue")
.attr("opacity", 0.5);
var bl = g.append("svg")
.attr('class', 'seg')
.attr("x", x(1))
.attr("y", y(3.5))
bl.append("rect")
.attr("width", x(6) - x(3.5))
.attr("height", y(3.5))
.attr("fill", "lightblue")
.attr("opacity", 0.5);
var tl = g.append("svg")
.attr('class', 'seg')
.attr("x", x(1))
.attr("y", y(6))
tl.append("rect")
.attr("width", x(6) - x(3.5))
.attr("height", y(3.5))
.attr("fill", "lightblue")
.attr("opacity", 0.5);
var tr = g.append("svg")
.attr('class', 'seg')
.attr("x", x(3.5))
.attr("y", y(6))
tr.append("rect")
.attr("class", "pos")
.attr("width", x(6) - x(3.5))
.attr("height", y(3.5))
.attr("fill", "lightblue")
.attr("opacity", 0.5);

///Legend ////
var segwidth = tr.select(".pos").attr("width")
tr.selectAll("mydots")
.data(["income","expenditure","staff_cost","counselling_cost"])
.enter()
.append("circle")
.attr("cx",segwidth/1.5)
.attr("cy", function(d,i){ return 75 + i*20}) // 100 is where the first dot appears. 25 is the distance between dots
.attr("r", 5)
.style("fill", function(d){ return color(d)})

// Add one dot in the legend for each name.
tr.selectAll("mylabels")
.data(["income","expenditure","staff_cost","counselling_cost"])
.enter()
.append("text")
.attr("x", (segwidth/1.5)+10)
.attr("y", function(d,i){ return 75 + i*20}) // 100 is where the first dot appears. 25 is the distance between dots
// .style("fill", function(d){ return color(d)})
.text(function(d){ return d})
.attr("text-anchor", "left")
.style("alignment-baseline", "middle")
.attr("font-family" , "sans-serif")
.attr("font-size" , "12px")
.attr("fill" , "black")

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

// tl.selectAll("mydots")
// .data(["counselling","staff_cost"])
// .enter()
// .append("circle")
// .attr("cx",(segwidth/1.5))
// .attr("cy", function(d,i){ return 60 + i*15}) // 100 is where the first dot appears. 25 is the distance between dots
// .attr("r", 5)
// .style("fill", function(d){ return colorTL(d)})

/// Add one dot in the legend for each name.
//tl.selectAll("mylabels")
// .data(["based on cost of service","based on staff cost"])
//.enter()
// .append("text")
// .attr("x", (segwidth/1.5) +10)
// .attr("y", function(d,i){ return 60 + i*15}) // 100 is where the first dot appears. 25 is the distance between dots
/// .style("fill", function(d){ return color(d)})
// .text(function(d){ return d})
// .attr("text-anchor", "left")
// .style("alignment-baseline", "middle")
// .attr("font-family" , "sans-serif")
// .attr("font-size" , "12px")
// .attr("fill" , "black")
////TEXT FIELDS
tl.append("text")
.attr("class", "testing")
.attr("x", width/4)
.attr("y", margin.top + 5)
.attr("dy", "1em")
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle")
.text("Counselling Unit cost (£) based on total expenditure");

tl.selectAll("text")
.each(function(d, i) { wrap_text_nchar(d3.select(this), 35) });


tr.append("text")
.attr("class", "testing")
.attr("x", width/3)
.attr("y", margin.top + 7)
.attr("dy", "1em")
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle")
.text("Charity financials compared with JDI ");

tr.selectAll("text")
.each(function(d, i) { wrap_text_nchar(d3.select(this), 50) });
br.append("text")
.attr("class", "testing")
.attr("x", width/3)
.attr("y", margin.top + 5)
.attr("dy", "1em")
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle")
.text("Charity Counselling unit cost(£) by Year");

br.selectAll("text")
.each(function(d, i) { wrap_text_nchar(d3.select(this), 28) });
//Chart TOP LEFT ////////////////////
tl.append("g").call(g => drawGuidelines(g, data.map(d => d.shortname),
d => d3.line()([[xScaleTL(d) + hx, margin.top],[xScaleTL(d) + hx, (height/2) - margin.bottom]]))
);

tl.append("g")
.selectAll(".bar")
.data(data)
.join("rect")
.attr('class', function (d) {return d.shortname + 'staff_HC' + ' bar'}) // d => d.shortname
.attr('id', function (d) {return (d.shortname + 'tl')})
.attr('id2', "staff_HC")
.attr('x', d => xScaleTL(d.shortname))
.attr('y', function (d,i) {if(d.unit_cost === 0) {i = d.unit_max} else {i = d.unit_cost} return yScaleTL(i)})
.attr('width', xScaleTL.bandwidth())
.attr('height', function (d,i) {if(d.unit_cost === 0) {i = d.unit_max} else {i = d.unit_cost} return yScaleTL(0) - yScaleTL(i)})
.style('fill', function (d) {return colorTL(d.algorithm)})
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.append('title')
.text(function(d){return d.charity;});

tl.append("g").selectAll(".desc")
.data(data)
.join("text")
.attr('class',"desc")
.text(function (d) {return d.missed})
.attr('x', d => xScaleTL(d.shortname) + xScaleTL.bandwidth()/1.5)
.attr('y', function (d) {return yScaleTL(d.unit_cost) - 5})
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle");

const gtl = tl.append("g")
.attr('class', 'x-axis')
.attr('transform', `translate(0,${ (height/2) - margin.bottom })`)
.call(xAxisTL);
gtl.transition(t)
.call(xAxisTL)
.selectAll("text")
.attr("class", "legend-text")
.attr("y", 0)
.attr("x", 9)
.attr('fill', 'navy')
.attr("transform", "rotate(90)")
.attr("text-anchor", "start");
tl.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "yellow")
.attr("stroke-width", 2.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
//.attr('marker-start', 'url(#dot)')
.attr("d", lineTL);

tl.append('g')
.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left },0)`)
.call( yAxisTL)



//Chart TOP LEFT ////////////////////END


//GENERAL////////////
const tr1 = tr.append("g")
.attr('class', 'x-axis')
.attr('transform', `translate(0,${ (height/2) - margin.bottom })`)
.call( xAxisTR )
const tr2 = tr.append('g')
.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left},0)`)
.call( yAxisTR )

const br1 = br.append("g")
.attr('class', 'x-axis')
.attr('transform', `translate(0,${ (height/2) - margin.bottom })`)
.call( xAxisBR )
const br2 = br.append('g')
.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left},0)`)
.call( yAxisBR )

const tl1 = tl.append("g")
.attr('class', 'x-axis')
.attr('transform', `translate(0,${ (height/2) - margin.bottom })`)
// .call( xAxisTL )
const tl2 = tl.append('g')
.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left},0)`)
// .call( yAxisTL )

const bl1 = bl.append("g")
.attr('class', 'x-axis')
.attr('transform', `translate(0,${ (height/2) - margin.bottom })`)
// .call( xAxisBL )
const bl2 = bl.append('g')
.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left},0)`)
// .call( yAxisBL )

//////GENERAL END/////////////


/// Chart BOTTOM LEFT///////////
bl.append("g").call(g => drawGuidelines(g, data.map(d => d.shortname),
d => d3.line()([[xScaleBL(d) + hx, margin.top],[xScaleBL(d) + hx, (height/2) - margin.bottom]]))
);


const bar = bl.append("g")
//.attr('class', "stacky")
.selectAll('g')
.data(stacked)
.join("g")
.style('fill', (d,i) => color(d.key))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr('x', function (d) { return xScaleBL(d.data.shortname) + xScaleBL.bandwidth()/3})
.attr('class', function (d) { return d.data.shortname + d.key})
.attr('id', function (d) { return d.data.shortname + d.key})
.attr('id2', function (d) { return d.key})
.attr('y', d => yScaleBL(d[1]))
.attr('height', d => yScaleBL(d[0]) - yScaleBL(d[1]))
.attr('width', xScaleBL.bandwidth()/3)
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.append('title')
.text(function(d){return d.data.charity;});
const gbl = bl.append("g")
.attr('class', 'x-axis')
.attr('transform', `translate(0,${ (height/2.2) - margin.bottom })`)
.call(xAxisBL);
gbl.transition(t)
.call(xAxisBL)
.selectAll("text")
.attr("class", "legend-text")
.attr("y", 0)
.attr("x", 9)
.attr('fill', 'navy')
.attr("transform", "rotate(90)")
.attr("text-anchor", "start");
// bl.append("path")
// .datum(data)
// .attr("fill", "none")
// .attr("stroke", "yellow")
// .attr("stroke-width", 2.5)
// .attr("stroke-linejoin", "round")
// .attr("stroke-linecap", "round")
// .attr("d", lineBL);

bl.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "yellow")
.attr("stroke-width", 2.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
//.attr('marker-start', 'url(#dot)')
.attr("d", lineBL2);
bl.append('g')
.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left },0)`)
.call( yAxisBL)

bl.append('g')
.attr('class', 'y-axis')
.style('fill', 'yellow')
.attr("font-family" , "sans-serif")
.style("shape-rendering", "crispEdges")
.attr("font-size" , "12px")
// .attr("stroke" , "yellow")
// .attr("stroke-width", 0.5)
.attr('transform', `translate(${(width/2) - margin.left-margin.right},0)`)
.call( yAxisBL2)

/////setup
xScaleTR.domain(["JDI","TIC"])
var datas = dynamic.filter(function(e) { return e.shortname === "JDI" || e.shortname === "TIC" })
yScaleTR.domain([0,d3.max(datas, d => d.param)])
tr.append('g')
.attr('class', 'bars')
.selectAll('rect')
.data(datas)
.join('rect')
.attr('class', 'bar')
.attr("id", d => d.shortname)
.attr('x', d => xScaleTR(d.shortname) + xBars(d.category))
.attr('y', function(d) {return yScaleTR(d.param)})
.attr('width', xBars.bandwidth())
.attr('height', function (d,i) { return yScaleTR(0) - yScaleTR(d.param)})
.style('fill', function(d) { return color(d.category) })
tr1.call(xAxisTR)
.transition()
.duration(1000)
.ease(d3.easeLinear);

tr2.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left},0)`)
.call( yAxisTR )
.transition()
.duration(1000)
.ease(d3.easeLinear);
//bottom left

//bottom right

// xScaleBR.domain(order)
//xScaleBR.domain(arr)
var fiveYr_filt = fiveYr.filter(function(e) { return "TIC" })
yScaleBR.domain([0,d3.max(fiveYr_filt, d => d.param)])
br.append('g')
.attr('class', 'bars')
.selectAll('rect')
.data(fiveYr_filt)
.join('rect')
.attr('class', 'bar')
.attr('x', d => xScaleBR(d.category) + xScaleBR.bandwidth()/4)
.attr('y', function(d) {return yScaleBR(d.param)})
.attr('width', xBars.bandwidth())
.attr('height', function (d,i) { return yScaleBR(0) - yScaleBR(d.param)})
.style('fill', function(d) {if(d.category === "income") { return "red"} else if(d.category === "expenditure") { return "blue"} else if (d.category === "staff_cost") {return "yellow"} else {return "orange"} })
br1.call(xAxisBR)
.transition()
.duration(1000)
.ease(d3.easeLinear);

br2.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left},0)`)
.call( yAxisBR )
.transition()
.duration(1000)
.ease(d3.easeLinear);

br.append("text")
.attr("class", "selection")
.attr("x", width/3)
.attr("y", margin.top + 50)
.attr("dy", "1em")
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle")
.text("Teens in Crisis");





/////setup
////BOTTON LEFT END/////////////////
function mouseout(event, d) {
var sel = d3.select(this).attr("id")
var sel2 = d3.select(this).attr("id2")
var act = tl.selectAll("." + sel)
if(sel === "tl") {
act.style("fill", colorTL(d.algorithm)) } else { //'#7472c0'
act.style("fill", colorTL(d.data.algorithm))} //'#7472c0'
if(sel2 === "staff_HC") {
var act2 = bl.selectAll("." + sel)
act2.style("fill", '#00ff40') }
}
function mouseover (event, d) {

br.selectAll(".selection").remove()
if(d.shortname == undefined) {d.shortname = d.data.shortname}
if(d.charity == undefined) {d.charity = d.data.charity}
br.append("text")
.attr("class", "selection")
.attr("x", width/3)
.attr("y", margin.top + 50)
.attr("dy", "1em")
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle")
.text(d.charity);



var sel1 = d3.select(this).attr("id")
// console.log(sel1)
var sel2 = d3.select(this).attr("id2")
// console.log(sel2)
// if(sel2 === "tl") {
var act = d3.selectAll("." + sel1)
act.style("fill", "yellow") //}
var arr = ["JDI"]
if (!arr.includes(d.shortname)){arr.push(d.shortname);}
xScaleTR.domain(order)
xScaleTR.domain(arr)
var datas = dynamic.filter(function(e) { return e.shortname === "JDI" || e.shortname === d.shortname })
tr.selectAll(".bars").remove()
yScaleTR.domain([0,d3.max(datas, d => d.param)])
tr.append('g')
.attr('class', 'bars')
.selectAll('rect')
.data(datas)
.join('rect')
.attr('class', 'bar')
.attr("id", d => d.shortname)
.attr('x', d => xScaleTR(d.shortname) + xBars(d.category))
.attr('y', function(d) {return yScaleTR(d.param)})
.attr('width', xBars.bandwidth())
.attr('height', function (d,i) { return yScaleTR(0) - yScaleTR(d.param)})
.style('fill', function(d) { return color(d.category) })
tr1.call(xAxisTR)
.transition()
.duration(1000)
.ease(d3.easeLinear);

tr2.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left},0)`)
.call( yAxisTR )
.transition()
.duration(1000)
.ease(d3.easeLinear);
//bottom left

//bottom right

// xScaleBR.domain(order)
//xScaleBR.domain(arr)
var fiveYr_filt = fiveYr.filter(function(e) { return e.shortname === d.shortname })
br.selectAll(".bars").remove()

yScaleBR.domain([0,d3.max(fiveYr_filt, d => d.param)])
br.append('g')
.attr('class', 'bars')
.selectAll('rect')
.data(fiveYr_filt)
.join('rect')
.attr('class', 'bar')
.attr('x', d => xScaleBR(d.category) + xScaleBR.bandwidth()/4)
.attr('y', function(d) {return yScaleBR(d.param)})
.attr('width', xBars.bandwidth())
.attr('height', function (d,i) { return yScaleBR(0) - yScaleBR(d.param)})
.style('fill', function(d) {if(d.category === "income") { return "red"} else if(d.category === "expenditure") { return "blue"} else if (d.category === "staff_cost") {return "yellow"} else {return "orange"} })
br1.call(xAxisBR)
.transition()
.duration(1000)
.ease(d3.easeLinear);

br2.attr('class', 'y-axis')
.attr('transform', `translate(${ margin.left},0)`)
.call( yAxisBR )
.transition()
.duration(1000)
.ease(d3.easeLinear);
}
return Object.assign(svg.node(), {
update(order2) {


bl.append("text")
.attr("class", "testing")
.attr("x", width/2.95)
.attr("y", margin.top * 3)
.attr("dy", "1em")
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle")
.text("Staff (green) - Volunteers (blue) Sessions/Yr (right axis)");

bl.selectAll("text")
.each(function(d, i) { wrap_text_nchar(d3.select(this), 25) });


const t = svg.transition()
.duration(1000);
var dataf = data.filter(function(e) {if(order2==="All"){return e } else { return e.service === order2}})

if(order2 === "All") { xScaleTL.domain([...new Set ((data.sort((a, b) => d3.descending(a.unit_cost, b.unit_cost) )).map(d => d.shortname))]);} else if (order2 === "CYW"){xScaleTL.domain([...new Set ((data.sort((a, b) => d3.descending(a.service, b.service)||d3.descending(a.unit_cost, b.unit_cost) )).map(d => d.shortname))])} else if (order2 === "C") {xScaleTL.domain([...new Set ((data.sort((a, b) => d3.ascending(a.service, b.service)||d3.descending(a.unit_cost, b.unit_cost) )).map(d => d.shortname))])}
gtl
.attr('class', 'x-axis')
.attr('transform', `translate(0,${ (height/2) - margin.bottom })`)
.call(xAxisTL);

gtl.transition(t)
.call(xAxisTL)
.selectAll("text")
.attr("class", "legend-text")
.attr("y", 0)
.attr("x", 9)
.attr('fill', 'navy')
.attr("transform", "rotate(90)")
.attr("text-anchor", "start");

const tx = tl.transition()
.duration(1000)
.ease(d3.easeBounce);
var bars = tl.selectAll(".bar").data(dataf, function(d) { return d.id; })
.join(enter => enter.append("rect")
.attr('class', function (d) {return d.shortname + 'staff_HC' + ' bar'}) // d => d.shortname
.attr('id', function (d) {return (d.shortname + 'tl')})
.attr('id2', "staff_HC")
.attr('x', d => xScaleTL(d.shortname))
.attr('y', function (d,i) {if(d.unit_cost === 0) {i = d.unit_max} else {i = d.unit_cost} return yScaleTL(i)})
.attr('width', xScaleTL.bandwidth())
.attr('height', function (d,i) {if(d.unit_cost === 0) {i = d.unit_max} else {i = d.unit_cost} return yScaleTL(0) - yScaleTL(i)})
.style('fill', function (d) {return colorTL(d.algorithm)})
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.append('title')
.text(function(d){return d.charity;})
.call(enter => enter.transition(tx)),
update => update
.call(update => update.transition(tx))
.attr('class', function (d) {return d.shortname + 'staff_HC' + ' bar'}) // d => d.shortname
.attr('id', function (d) {return (d.shortname + 'tl')})
.attr('id2', "staff_HC")
.attr('x', d => xScaleTL(d.shortname))
.attr('y', function (d,i) {if(d.unit_cost === 0) {i = d.unit_max} else {i = d.unit_cost} return yScaleTL(i)})
.attr('width', xScaleTL.bandwidth())
.attr('height', function (d,i) {if(d.unit_cost === 0) {i = d.unit_max} else {i = d.unit_cost} return yScaleTL(0) - yScaleTL(i)})
.style('fill', function (d) {return colorTL(d.algorithm)})
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.append('title')
.text(function(d){return d.charity;}),
exit => exit
.call(exit => exit.transition(tx)
.remove()));
// ));

var text = tl.selectAll(".desc").data(dataf)
.join(enter => enter.append("text")
.text(function (d) {return }) //d.missed}
.attr('x', d => xScaleTL(d.shortname) + xScaleTL.bandwidth()/1.5)
.attr('y', function (d) {return yScaleTL(d.unit_cost) - 5})
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle")
.attr('class','desc')
.call(enter => enter.transition(t)),
update => update
.call(update => update.transition(t))
.text(function (d) {return })
.attr('x', d => xScaleTL(d.shortname) + xScaleTL.bandwidth()/1.5)
.attr('y', function (d) {return yScaleTL(d.unit_cost) - 5})
.attr("font-family" , "sans-serif")
.attr("font-size" , "14px")
.attr("fill" , "black")
.attr("text-anchor", "middle")
.attr('class','desc'),
exit => exit
.call(exit => exit.transition(t)
.remove()));

tl.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "yellow")
.attr("stroke-width", 2.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
//.attr('marker-start', 'url(#dot)')
.attr("d", lineTL);
}
})
}

Insert cell
mutable order =[]
Insert cell
update = chart.update(order2)
Insert cell
height = 600
Insert cell
width = 1000
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
datar = dynamic.filter(function(d) { return d.shortname === "JDI"})
Insert cell
Insert cell
dynamic = {
var arr = []
const columns = data.columns.slice(3,7);
var x = 0
data.forEach(function (d) {
for (x = 0; x < columns.length; x++) {
arr.push({"shortname": d.shortname, "category": columns[x], "param": +d[columns[x]]})
}
})
return arr
}
Insert cell
stacked = stack(data).map(d => (d.forEach(v => v.key = d.key), d))
Insert cell
Insert cell
data = d3.csv(getCsvUrl(url),d3.autoType) //.sort((a, b) => d3.ascending(a.unit_cost, b.unit_cost)))
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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