Public
Edited
May 6, 2023
Fork of Simple D3
Insert cell
Insert cell
chart_90 = {
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

svg
.selectAll("circle")
.data(d3.range(10))
.join("circle")
.attr("cx", d => d * 100)
.attr("cy", height / 2)
.attr("r", d => 1 * 20)
.attr("fill", "hsl(216deg 100% 13%)");

svg.append("line")
.attr("x1",100)
.attr("x2", 400)
.attr("y1", 40)
.attr("y2", 40)
.attr("stroke", 'navy');

return svg.node();
}
Insert cell
chart2 = {const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const data = d3.range(10);

const circle = svg.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", d => d * 100)
.attr("cy", height / 2)
.attr("r", d => 1 * 20)
.attr("fill", "hsl(216deg 100% 13%)");

const line = d3.line()
.x((d) => d.x)
.y((d) => d.y);

const path = svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", "gray")
.attr("stroke-width", 2)
.attr("marker-end", "url(#arrow)")
.attr("d", (d) => line(d.map((e) => ({x: e * 100 + 20, y: height / 2}))));

const marker = svg.append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M0,-5L10,0L0,5");

return svg.node();
}
Insert cell
height = 1200
Insert cell
width = 400
Insert cell
chart_3 = {const data = d3.range(10);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);

const g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

const nodes = g.selectAll(".node")
.data(data)
.enter()
.append("circle")
.attr("class", "node")
.attr("cx", 0)
.attr("cy", (d) => d * 100)
.attr("r", 20)
.attr("fill", "gray")
.on("click", function(d, i) {
d3.select(this)
.transition()
.duration(500)
.attr("fill", "blue");
if (i < data.length - 1) {
d3.select(nodes.nodes()[i + 1])
.transition()
.delay(500)
.duration(500)
.attr("fill", "navy");
}
});

const lines = g.selectAll(".line")
.data(data.slice(0, -1))
.enter()
.append("path")
.attr("class", "line")
.attr("stroke", "gray")
.attr("stroke-width", 2)
.attr("marker-end", "url(#arrow)")
.attr("d", (d) => "M0," + (d * 100) + "L0," + ((d + 1) * 100));

const marker = svg.append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M0,-5L10,0L0,5");

return svg.node();
}
Insert cell
chart_4 =
{const data = d3.range(10);
const labels = ["Open Netflix account", "Insert the information ", "click on the other text", "Fourth node", "Fifth node", "Sixth node", "Seventh node", "Eighth node", "Ninth node", "Tenth node"];

const nodeRadius = 20;
const nodeSpacing = 1500; // increased spacing between nodes
const height1 = 1200;
const width1 = 1200;

const svg = d3.create("svg")
.attr("width", width1)
.attr("height", height1);

const g = svg.append("g")
.attr("transform", "translate(" + width1 / 2 + "," + height1 / 8+ ")");

// const nodes = g.selectAll(".node")
// .data(data)
// .enter()
// .append("g")
// .attr("class", "node")
// .attr("transform", (d) => "translate(0," + (d * 100) + ")")
// .on("click", function(d, i) {
// d3.select(this)
// .select("circle")
// .attr("fill", "navy")
// .transition()
// .duration(10)
// .attr("r", 25);
// d3.select(this)
// .select("text")
// .attr("fill", "navy");
// if (i < data.length - 1) {
// d3.select(lines.nodes()[i])
// .attr("stroke", "navy");
// d3.select(nodes.nodes()[i + 1])
// .select("circle")
// .attr("fill", "navy")
// .transition()
// .delay(500)
// .duration(500)
// .attr("r", 25);
// d3.select(nodes.nodes()[i + 1])
// .select("text")
// .attr("fill", "navy");
// }
// });
const nodes = g.selectAll(".node")
.data(data)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", (d) => "translate(0," + (d * 100) + ")")
.on("click", function(d, i) {
d3.select(this)
.select("circle")
.attr("fill", "navy")
.transition()
.duration(10)
.attr("r", 25);
d3.select(this)
.select("text")
.style("font-weight", "bold")
.attr("fill", "navy");
if (i < data.length - 1) {
d3.select(lines.nodes()[i])
.attr("stroke", "navy");
d3.select(nodes.nodes()[i + 1])
.select("circle")
.attr("fill", "navy")
.transition()
.delay(500)
.duration(500)
.attr("r", 25);
d3.select(nodes.nodes()[i + 1])
.select("text")
.style("font-weight", "bold")
.attr("fill", "navy");
}
})
.on("mouseover", function(d, i) {
d3.select(this)
.select("circle")
.attr("fill", "lightgray");
d3.select(this)
.select("text")
.style("font-weight", "bold")
.attr("fill", "black");
})
.on("mouseout", function(d, i) {
d3.select(this)
.select("circle")
.attr("fill", "lightgray");
d3.select(this)
.select("text")
.style("font-weight", "normal")
.attr("fill", "black");
});


nodes.append("circle")
.attr("r", nodeRadius)
.attr("fill", "gray");

nodes.append("text")
.attr("x", 30)
.attr("y", 5)
.text((d, i) => labels[i]);

const lines = g.selectAll(".line")
.data(data.slice(0, -1))
.enter()
.append("path")
.attr("class", "line")
.attr("stroke", "gray")
.attr("stroke-width", 2)
.attr("marker-end", "url(#arrow)")
.attr("d", (d) => "M0," + (d * 100 + nodeRadius) + "L0," + ((d + 1) * 100 - nodeRadius));

const marker = svg.append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M0,-5L10,0L0,5");

return svg.node();
}
Insert cell
chart_ =
{const data = d3.range(10);
const labels = ["Open Netflix account", "Insert the information ", "click on the other text", "Fourth node", "Fifth node", "Sixth node", "Seventh node", "Eighth node", "Ninth node", "Tenth node"];

const nodeRadius = 30;
const nodeSpacing = 200; // increased spacing between nodes
const height1 = 1200;
const width1 = 1200;

const svg = d3.create("svg")
.attr("width", width1)
.attr("height", height1);

const g = svg.append("g")
.attr("transform", "translate(" + width1 / 2 + "," + height1 / 8+ ")");

const nodes = g.selectAll(".node")
.data(data)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", (d) => "translate(0," + (d * nodeSpacing) + ")")
.on("mouseover", function() {
d3.select(this)
.select("circle")
.attr("fill", "navy")
.attr("r", nodeRadius +5);
d3.select(this)
.select("text")
.attr("fill", "navy")
.attr("font-size", "40px")
.attr("font-weight", "bold");
})
.on("mouseout", function() {
d3.select(this)
.select("circle")
.attr("fill", "#ccc")
.attr("r", nodeRadius);
d3.select(this)
.select("text")
.attr("fill", "grey")
.attr("font-size", "10px")
.attr("font-weight", "normal");
});

nodes.append("circle")
.attr("r", nodeRadius)
.attr("fill", "gray");

nodes.append("text")
.attr("x", nodeRadius+ 10)
.attr("y", 5)
.attr("fill", "grey")
.attr("font-size", "12px")
.text((d, i) => labels[i]);

const lines = g.selectAll(".line")
.data(data.slice(0, -1))
.enter()
.append("path")
.attr("class", "line")
.attr("stroke", "gray")
.attr("stroke-width", 2)
.attr("marker-end", "url(#arrow)")
.attr("d", (d) => "M0," + (d * nodeSpacing + nodeRadius) + "L0," + ((d + 1) * nodeSpacing - nodeRadius));

const marker = svg.append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M0,-5L10,0L0,5");

return svg.node();
}
Insert cell
chart_5 = {
const data = d3.range(10);
const labels = ["First node", "Second node", "Third node", "Fourth node", "Fifth node", "Sixth node", "Seventh node", "Eighth node", "Ninth node", "Tenth node"];

const nodeRadius = 20;
const nodeSpacing = 1500; // increased spacing between nodes
const height1 = 8400;
const width1 = 1200;
const visited = new Array(data.length).fill(false);
const svg = d3.create("svg")
.attr("width", width1)
.attr("height", height1);

const g = svg.append("g")
.attr("transform", "translate(" + width1/2 + "," + height1 / 15 + ")");

nodeSpacing

const nodes = g.selectAll(".node")
.data(data)
.enter()
.append("g")
.attr("class", "node")
// .attr("transform", (d) => "translate(0," + (d * 100) + ")");
.attr("transform", (d) => "translate(0," + (d * nodeSpacing) + ")");

nodes.append("circle")
.attr("r", 20)
.attr("fill", "gray");

nodes.append("text")
.attr("x", nodeRadius + 10)
// .attr("x", 30)
.attr("y", 5)
.text((d, i) => labels[i]);


const lines = g.selectAll(".line")
.data(data.slice(0, -1))
.enter()
.append("path")
.attr("class", "line")
.attr("stroke", "gray")
.attr("stroke-width", 2)
.attr("marker-end", "url(#arrow)")
//.attr("d", (d) => "M0," + (d * 100 + 20) + "L0," + ((d + 1) * 100 - 20));
// .attr("d", (d) => "M0," + (d * 150 + 30) + "L0," + ((d + 1) * 150 - 30));
.attr("d", (d) => "M0," + (d * nodeSpacing + nodeRadius) + "L0," + ((d + 1) * nodeSpacing - nodeRadius));

const marker = svg.append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M0,-5L10,0L0,5");

let currentIndex = 0;

// function updateNodes() {
// nodes
// .select("circle")
// .attr("fill", (d, i) => i === currentIndex ? "navy" : "gray");

// nodes
// .select("text")
// .attr("fill", (d, i) => i === currentIndex ? "navy" : "blue");

// lines
// .attr("stroke", (d, i) => i < currentIndex ? "navy" : "gray");
// }

// function updateNodes() {
// nodes
// .select("circle")
// .attr("fill", (d, i) => i === currentIndex ? "navy" : "gray");

// nodes
// .select("text")
// .attr("fill", (d, i) => i === currentIndex ? "navy" : "black")
// .style("font-weight", (d, i) => i === currentIndex ? "bold" : "normal");

// lines
// .attr("stroke", (d, i) => i < currentIndex ? "navy" : "gray");

// }

// function updateNodes() {
// const prevIndex = currentIndex - 1 >= 0 ? currentIndex - 1 : 0;
// nodes.select("circle")
// .attr("fill", (d, i) => i === currentIndex ? "navy" : (i === prevIndex ? "gray" : "lightgray"))
// .classed("current", (d, i) => i === currentIndex);
// nodes.select("text")
// .attr("fill", (d, i) => i === currentIndex ? "navy" : "black");
// lines.attr("stroke", (d, i) => i < currentIndex ? "navy" : "gray");
// }

function updateNodes() {
nodes
.select("circle")
.attr("fill", (d, i) => {
if (i === currentIndex) {
visited[i] = true;
return "navy";
} else if (visited[i]) {
return "navy";
} else {
return "gray";
}
});

nodes
.select("text")
.attr("fill", (d, i) => i === currentIndex ? "navy" : "black");

lines
.attr("stroke", (d, i) => i < currentIndex ? "navy" : "gray");
}


updateNodes();

window.addEventListener("wheel", (event) => {
if (event.deltaY < 0 && currentIndex > 0) {
currentIndex--;
updateNodes();
} else if (event.deltaY > 0 && currentIndex < data.length - 1) {
currentIndex++;
updateNodes();
}
});

return svg.node();
}
Insert cell
chart = {
let currentTransform = [width / 2, height / 2, height];

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

const g = svg.append("g");

g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", ([x]) => x)
.attr("cy", ([, y]) => y)
.attr("r", radius)
.attr("fill", (d, i) => d3.interpolateRainbow(i / 360))

function transition() {
const d = data[Math.floor(Math.random() * data.length)];
const i = d3.interpolateZoom(currentTransform, [...d, radius * 2 + 1]);

g.transition()
.delay(250)
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)))
.on("end", transition);
}

function transform([x, y, r]) {
return `
translate(${width / 2}, ${height / 2})
scale(${height / r})
translate(${-x}, ${-y})
`;
}

return svg.call(transition).node();
}
Insert cell
chart_8 = {
let currentTransform = [width / 2, height / 2, height];

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

const g = svg.append("g");

const data = Array.from({length: 15}, () => [
Math.random() * width,
Math.random() * height
]);

g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", ([x]) => x)
.attr("cy", ([, y]) => y)
.attr("r", radius)
.attr("fill", (d, i) => d3.interpolateRainbow(i / 360))
.on("mousein", function(d) {
const i = d3.interpolateZoom(currentTransform, [...d, radius * 4 + 1]);
g.transition()
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)));
})
.on("mousehover", function(d) {
const i = d3.interpolateZoom(currentTransform, [width / 2, height / 2, height]);
g.transition()
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)));
});

function transition() {
const d = data[Math.floor(Math.random() * data.length)];
const i = d3.interpolateZoom(currentTransform, [...d, radius * 2 + 1]);

g.transition()
.delay(2500)
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)))
.on("end", transition);
}

function transform([x, y, r]) {
return `
translate(${width / 2}, ${height / 2})
scale(${height / r})
translate(${-x}, ${-y})
`;
}

return svg.call(transition).node();
}

Insert cell
radius = 6
Insert cell
step = radius * 2
Insert cell
data = Array.from({length: 2000}, (_, i) => {
const r = step * Math.sqrt(i += 0.5), a = theta * i;
return [
width / 2 + r * Math.cos(a),
height / 2 + r * Math.sin(a)
];
})
Insert cell
theta = Math.PI * (3 - Math.sqrt(5))
Insert cell
d3 = require("d3@6")
Insert cell
chart_9 = {
let currentTransform = [width / 8, height / 8, height];

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

const g = svg.append("g");

const data = Array.from({length: 15}, () => [
Math.random() * width,
Math.random() * height
]);

g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", ([x]) => x)
.attr("cy", ([, y]) => y)
.attr("r", radius)
.attr("fill", (d, i) => d3.interpolateRainbow(i / 360))
.on("mouseover", function(d) {
const i = d3.interpolateZoom(currentTransform, [...d, radius * 4 + 1]);
g.transition()
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)));
})
.on("mouseout", function(d) {
const i = d3.interpolateZoom(currentTransform, [width / 2, height / 2, height]);
g.transition()
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)));
});

function transition() {
const d = data[Math.floor(Math.random() * data.length)];
//const i = d3.interpolateZoom(currentTransform, [...d, radius * 2 + 1]);

g.transition()
.delay(250)
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)))
.on("end", transition);
}

function transform([x, y, r]) {
return `
translate(${width / 2}, ${height / 2})
scale(${height / r})
translate(${-x}, ${-y})
`;
}

return svg.call(transition).node();
}

Insert cell
exerciseMargin = ({top: 15, right: 15, bottom: 30, left: 50})
Insert cell
exerciseWidth = 700
Insert cell
exerciseHeight = 400
Insert cell
// set up zoom behavior
const zoom = d3.zoom()
.extent([[0, 0], [exerciseWidth, exerciseHeight]])
.translateExtent([[0, 0], [exerciseWidth, exerciseHeight]])
.scaleExtent([1, 10])
.on('zoom', zoomed);

// add the zoom behavior to the SVG element
svg.call(zoom);

function zoomed(event) {
// get the current transform of the SVG element
const transform = d3.event.transform;

// update the position and radius of each circle based on the current transform
g.selectAll('circle')
.attr('transform', transform)
.attr('r', radius / transform.k);
}

// handle hovering over a circle
function mouseEnter(event, d) {
// make the circle larger
d3.select(this)
.attr('r', radius * 2)
.attr("fill", "purple");

// update the label's text and get its width
tooltipText.text(d.precip);
const labelWidth = tooltipText.node().getComputedTextLength();

// set the width of the tooltip's background rectangle
// to match the width of the label, plus some extra space
tooltipRect.attr('width', labelWidth + 6);

// move the tooltip to the position of the circle (offset by a bit)
// and make the tooltip visible
const xPos = exerciseX(d.avg_temp) + radius * 3;
const yPos = exerciseY(d.precip) - tooltipHeight / 2;

tooltip.attr('transform', `translate(${xPos},${yPos})`)
.attr('visibility', 'visible');
}

// handle leaving a circle
function mouseLeave(event, d) {
// reset the size of the circle
d3.select(this)
.attr('r', radius)
.attr("fill", "gray")

// make the tooltip invisible
tooltip
.attr('visibility', 'hidden');
}
Insert cell

tooltips = {
const svg = d3.create('svg')
// add more margin to the right to account for the tooltips
.attr('width', exerciseWidth + exerciseMargin.left + exerciseMargin.right + 100)
.attr('height', exerciseHeight + exerciseMargin.top + exerciseMargin.bottom);

const g = svg.append('g')
.attr('transform', `translate(${exerciseMargin.left}, ${exerciseMargin.top})`);
// axes
g.append("g").call(exerciseXAxis, exerciseX, 'Average Temperature');
g.append("g").call(exerciseYAxis, exerciseY, 'Precipitation');
// draw points
const radius = 3;
const circles = g.selectAll('circle')
.data(example)
.join('circle')
.attr('cx', d => exerciseX(d.avg_temp))
.attr('cy', d => exerciseY(d.precip))
.attr('fill', d => examplecolor(d.origin))
.attr('r', radius)
// ********** new stuff starts here **********
.on('mouseenter', mouseEnter)
.on('mouseleave', mouseLeave);
// create tooltip
const tooltip = g.append('g')
.attr('visibility', 'hidden');
const tooltipHeight = 16;
// add a rectangle to the tooltip to serve as a background
const tooltipRect = tooltip.append('rect')
.attr('fill', 'black')
.attr('rx', 5)
.attr('height', tooltipHeight);
// add a text element to the tooltip to contain the label
const tooltipText = tooltip.append('text')
.attr('fill', 'white')
.attr('font-family', 'sans-serif')
.attr('font-size', 12)
.attr('y', 2) // offset it from the edge of the rectangle
.attr('x', 3) // offset it from the edge of the rectangle
.attr('dominant-baseline', 'hanging')
// add zoom functionality
const zoom = d3.zoom()
.scaleExtent([1, 10])
.on('zoom', zoomed);

svg.call(zoom);

function zoomed(event) {
const {transform} = event;
g.attr('transform', transform);
}
// handle hovering over a circle
function mouseEnter(event, d) {
// make the circle larger
d3.select(this)
.attr('r', radius * 2)
.attr("fill", "purple");
// update the label's text and get its width
tooltipText.text(d.precip);
const labelWidth = tooltipText.node().getComputedTextLength();
// set the width of the tooltip's background rectangle
// to match the width of the label, plus some extra space
tooltipRect.attr('width', labelWidth + 6);
// move the tooltip to the position of the circle (offset by a bit)
// and make the tooltip visible
const xPos = exerciseX(d.avg_temp) + radius * 3;
const yPos = exerciseY(d.precip) - tooltipHeight / 2;

tooltip.attr('transform', `translate(${xPos},${yPos})`)
.attr('visibility', 'visible');
}
Insert cell
chart_10 = {
const numBubbles = 15;
const bubblePadding = 20;
let currentTransform = [width / 2, height / 2, height];

const data = Array.from({ length: numBubbles }, (_, i) => {
const angle = i / numBubbles * Math.PI * 2;
return [Math.cos(angle) * (width / 4) + width / 2, Math.sin(angle) * (height / 4) + height / 2];
});

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

const g = svg.append("g");

g.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", ([x]) => x)
.attr("cy", ([, y]) => y)
.attr("r", radius)
.attr("fill", (d, i) => d3.interpolateRainbow(i / 360))
.attr("transform", "scale(0.1)")
.on("mouseover", function() {
const i = d3.interpolateZoom(currentTransform, [...d3.select(this).datum(), radius * 2 + bubblePadding]);

g.transition()
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)));
})
.on("mouseout", function() {
const i = d3.interpolateZoom(currentTransform, [width / 2, height / 2, height]);

g.transition()
.duration(i.duration)
.attrTween("transform", () => t => transform(currentTransform = i(t)));
});

function transform([x, y, r]) {
return `
translate(${width / 2}, ${height / 2})
scale(${height / r})
translate(${-x}, ${-y})
`;
}

return svg.node();
}

Insert cell
movies = FileAttachment("movies@2.csv").csv({typed: true})
Insert cell
// make another example (correlation between gross total and rating)
corr_plot = {

const width = 650
const height = 400
const margin = ({top: 15, right: 15, bottom: 20, left: 30})
const axisColor = "#404040"

const filtered = movies.filter(d => d.gross_total != NaN)

// create the container
const svg = d3.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);

// create scales
const x = d3.scaleLinear()
.domain([7, 10])
.range([margin.left, width])

const y = d3.scaleLinear()
.domain([0, d3.max(movies, d=> d.gross_total)])
.range([height, margin.top])

// create axes
const xAxis = svg.append("g")
.attr("transform", `translate(0 ,${height})`)
.call(d3.axisBottom()
.scale(x)
.ticks(10))
.attr("color", axisColor)

const yAxis = svg.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft()
.scale(y)
.ticks(10)
)
.attr("color", axisColor)

// add the points
const data = svg.append("g")
.attr("id", "dataPoints")
.selectAll("circle")
.data(filtered)
.join("circle")
.attr("r", 4)
.attr("cx", d => x(d.imdb_rating))
.attr("cy", d => y(d.gross_total))
.attr("fill", d => d["gross_total"] > 400 ? "steelblue" : "gray")

return svg.node()

}

// if we wanted to add a linear regression
// https://observablehq.com/@hydrosquall/simple-linear-regression-scatterplot-with-d3
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