Public
Edited
Mar 23, 2023
Insert cell
Insert cell
Insert cell
Insert cell
unTyped = FileAttachment("penguins_without_nan.csv").csv()
Insert cell
unTypedFirst = unTyped[0]
Insert cell
unTypedFirst.bill_depth_mm + unTypedFirst.bill_length_mm

Insert cell
Insert cell
penData = FileAttachment("penguins_without_nan.csv").csv({ typed: true })
Insert cell
penData.columns
Insert cell
Insert cell
manuData = {
const text = await FileAttachment("penguins_without_nan.csv").text()
return d3.csvParse(
text,
({
id,
species,
island,
bill_length_mm,
bill_depth_mm,
flipper_length_mm,
body_mass_g,
sex
}) => ({ // maps each row of the CSV data to a new object with the same properties, but modifying the type
id: parseInt(id),
species,
island,
bill_length_mm: parseFloat(bill_length_mm),
bill_depth_mm: parseFloat(bill_depth_mm),
flipper_length_mm: parseFloat(flipper_length_mm),
body_mass_g: parseFloat(body_mass_g),
sex
})
);
}
Insert cell
Insert cell
import { SummaryTable } from "@observablehq/summary-table"
Insert cell
viewof summary_data = SummaryTable(penData, { label: "Penguins Data" })
Insert cell
summary_data
Insert cell
viewof iris_data_summary = SummaryTable(
await FileAttachment("iris.csv").csv({ typed: true }),
{
label: "Iris Data"
}
)
Insert cell
Insert cell
import {Wrangler, op} from "@observablehq/data-wrangler"
Insert cell
irisPart1 = FileAttachment("iris_part1.csv").csv({ typed: true }) // first half of the iris data
Insert cell
irisPart2 = FileAttachment("iris_part2.csv").csv({ typed: true }) // second half of the iris data
Insert cell
Insert cell
Wrangler(irisPart1, irisPart2)
Insert cell
// To use copied code replace "data" with your own variable
aq.from(data)
.union(aq.from(data2))
// .objects() // Uncomment to return an array of objects
Insert cell
// To use copied code replace "data" with your own variable
combinedDataTable =
aq.from(irisPart1)
.union(aq.from(irisPart2))
.objects() // Uncomment to return an array of objects
Insert cell
Insert cell
Insert cell
// see https://observablehq.com/@observablehq/htl
htl.html`<svg width="300px" height="150px">
<circle cx="50" cy="50" r="20" fill="blue"/>
<circle cx="100" cy="80" r="15" fill="red"/>
</svg>`
Insert cell
htl.html`<svg width="300px" height="150px">
<circle cx="50" cy="50" r="20" fill="blue" transform="translate(50,0)"/> // the circle is translated 50 pixels to the right
<circle cx="100" cy="80" r="15" fill="red"/>
</svg>`
Insert cell
Insert cell
htl.html`<svg viewBox="0 0 600 300" width="300px" height="150px"> // viewBox attribute defines position and dimensions of the SVG canvas
<circle cx="50" cy="50" r="20" fill="blue" transform="translate(50,0)"/>
<circle cx="100" cy="80" r="15" fill="red"/>
</svg>`
Insert cell
Insert cell
htl.html`<svg width="300px" height="150px">
<g transform="translate(50,0)"> // a group element that is translated 50 pixels to the right
<circle cx="50" cy="50" r="20" fill="blue"/>
<circle cx="100" cy="80" r="15" fill="red"/>
<g>
</svg>`
Insert cell
Insert cell
htl.html`<svg style="width:450px; height:200px;">
${penData.map(
(d) =>
htl.svg`<circle cx="${d.bill_length_mm}" cy="${d.bill_depth_mm}" r="1"></circle>` // Create a circle element for each data point in the data
)}
</svg>`

Insert cell
maxBillLen = d3.max(penData, (d) => d.bill_length_mm)
Insert cell
maxBillDep = d3.max(penData, (d) => d.bill_depth_mm)
Insert cell
// store the element in scatter
scatter = htl.html`<svg viewBox="0 0 ${maxBillLen+2} ${maxBillDep+2}" style="width:450px; height:200px;"> // dynamically set the w and h
${penData.map(
(d) =>
htl.svg`<circle class="p-point" cx="${d.bill_length_mm}" cy="${d.bill_depth_mm}" r="0.5"></circle>` // use the circle css class. also smaller radius
)}
</svg>`
Insert cell
Insert cell
style = html`
<style>
.p-point {
stroke: orange;
stroke-width: 0.2px;
opacity: 0.25;
}
</style>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
svgSel = d3.select(scatter) // select the scatter SVG element
.selectAll("circle") // select all circle elements inside the SVG element
.style("fill", "#3EB489") // Set the fill property of all circle elements to the color #3EB489 (mint green)

Insert cell
svgSel.nodes() // Retrieve the DOM nodes corresponding to the D3 selection `svgSel`
Insert cell
Insert cell
import { Histogram } from "@d3/histogram"
Insert cell
penMass = penData.map((elem) => elem.body_mass_g)
Insert cell
Histogram(penMass)
Insert cell
Histogram(penMass, {width, height: 200, color: "steelblue"})
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
viewof scrub = Scrubber(d3.ticks(-5,5,200), {
format: (x) => `scrub = ${d3.format("+.2f")(x)}`,
autoplay: false,
alternate: true
})
Insert cell
randoms = Float64Array.from({length: 2000}, d3.randomNormal(scrub, 2))
Insert cell
Histogram(randoms, { width, height: 200, color: "steelblue", domain: [-10, 10] })
Insert cell
Insert cell
penAttrNumerical = Object.keys(penData[0]).filter(
(a) => typeof penData[0][a] === "number"
)
Insert cell
penAttr = Object.keys(penData[0])
.filter((a) => typeof penData[0][a] === "number")
.slice(1)

Insert cell
// viewof select = Inputs.select(["A", "B"], {label: "Select one"})
Insert cell
// this was created using the select input snippet
viewof xAttrName = Inputs.select(penAttr, {label: "X Attribute", value: penAttr[0]})
Insert cell
viewof yAttrName = Inputs.select(penAttr, {label: "Y Attribute", value: penAttr[1]})
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("viewBox", [0,0,width,height]);
svg
.selectAll("circle") // selects all the existing circle elements or empty selection
.data(penData) // binds the penData dataset to the selected circle elements
.join("circle") // creates, updates, or removes circle elements based on the bound data
.attr("cx", (d) => d[xAttrName])
.attr("cy", (d) => d[yAttrName])
.attr("r", 3)
return svg.node(); // returns the DOM node of the SVG container such that observable can show it
}
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("viewBox", [0,0,width,height]);
const xScale = d3
.scaleLinear() // create a new linear scale - a function that maps input to output at a different scale linearly
.domain(d3.extent(penData, (d) => d[xAttrName])) // sets the input domain of the xScale getting min and max of xAttrName
.range([0, width]) // sets the output range of the xScale
const yScale = d3
.scaleLinear() // create a new linear scale - a function that maps input to output at a different scale linearly
.domain(d3.extent(penData, (d) => d[yAttrName])) // sets the input domain of the xScale getting min and max of xAttrName
.range([0, height]) // sets the output range of the xScale

svg
.selectAll("circle")
.data(penData)
.join("circle")
.attr("cx", (d) => xScale(d[xAttrName])) // using the scaled value for x
.attr("cy", (d) => yScale(d[yAttrName])) // using the scaled value for y
.attr("r", 3)
return svg.node(); // returns the DOM node of the SVG container such that observable can show it
}
Insert cell
Insert cell
margin = ({ top: 20, right: 20, bottom: 20, left: 20 })
Insert cell
{
const svg = d3.create("svg").attr("viewBox", [0,0,width,height]);
const iWidth = width - margin.left - margin.right // define new width that takes the margins into account
const iHeight = height - margin.top - margin.bottom // define new height that takes the margins into account
const xScale = d3
.scaleLinear()
.domain(d3.extent(penData, (d) => d[xAttrName]))
.range([0, iWidth]); // use new iWidth
const yScale = d3
.scaleLinear()
.domain(d3.extent(penData, (d) => d[yAttrName]))
.range([0, iHeight]); // use new iHeight

// define the circles as a group and translate them to take the margin into account
const g = svg.append("g").attr("transform",`translate(${margin.left},${margin.top})`)

g // using the group instead of the svg
.selectAll("circle")
.data(penData)
.join("circle")
.attr("cx", (d) => xScale(d[xAttrName]))
.attr("cy", (d) => yScale(d[yAttrName]))
.attr("r", 3)
return svg.node();
}
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const iWidth = width - margin.left - margin.right; // define new width that takes the margins into account
const iHeight = height - margin.top - margin.bottom; // define new height that takes the margins into account

const xScale = d3
.scaleLinear()
.domain(d3.extent(penData, (d) => d[xAttrName]))
.range([0, iWidth])
.nice(); // called on the scale to adjust the domain to include "nice", round, human-readable values (for axis tick values)

const yScale = d3
.scaleLinear()
.domain(d3.extent(penData, (d) => d[yAttrName]))
.range([0, iHeight]) // use new iHeight
.nice(); // called on the scale to adjust the domain to include "nice", round, human-readable values (for axis tick values)

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

g.append("g").call(d3.axisTop(xScale));
g.append("g").call(d3.axisLeft(yScale));

g.selectAll("circle")
.data(penData)
.join("circle")
.attr("cx", (d) => xScale(d[xAttrName]))
.attr("cy", (d) => yScale(d[yAttrName]))
.attr("r", 3);
return svg.node();
}
Insert cell
Insert cell
// import {Legend, Swatches} from "@d3/color-legend"
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const iWidth = width - margin.left - margin.right;
const iHeight = height - margin.top - margin.bottom;

const xScale = d3
.scaleLinear()
.domain(d3.extent(penData, (d) => d[xAttrName]))
.range([0, iWidth])
.nice();

const yScale = d3
.scaleLinear()
.domain(d3.extent(penData, (d) => d[yAttrName]))
.range([0, iHeight])
.nice();

const cScale = d3 // define a color scale
.scaleOrdinal(d3.schemeCategory10) // ordinal scale with color scheme d3.schemeCategory10 - array of 10 categorical colors. used as output range
.domain(new Set(penData.map((e) => e.species)).values()); // set input domain of cScale using the uníque set of species as an iterator

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

g.append("g").call(d3.axisTop(xScale));
g.append("g").call(d3.axisLeft(yScale));

g.selectAll("circle")
.data(penData)
.join("circle")
.attr("fill", (d) => cScale(d.species)) // using the species attribute and plugging it into the color scale for the fill property
.attr("cx", (d) => xScale(d[xAttrName]))
.attr("cy", (d) => yScale(d[yAttrName]))
.attr("r", 3);
return svg.node();
}

Insert cell
Insert cell
data_concept = FileAttachment("data_concept.png").image()
Insert cell
alphabet = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
Insert cell
// This graphic is static in the sense that it is created from scratch each time the cell runs, making the D3 code equivalent in spirit to an HTML literal
chartStatic = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, 33])
.attr("font-family", "sans-serif")
.attr("font-size", 16);

// bind alphabet to text elements, create them if they don't exist, and set their attributes and text content
svg
.selectAll("text")
.data(alphabet)
.join("text")
.attr("x", (d, i) => i * 17)
.attr("y", 17)
.attr("dy", "0.35em")
.text((d) => d);

return svg.node();
}
Insert cell
// equivalent static html:
htl.html`<svg viewBox="0 0 ${width} 33" font-family="sans-serif" font-size="16">
${alphabet.map(
(d, i) => htl.svg`<text x="${i * 17}" y="17" dy="0.35em">${d}</text>`
)}
</svg>`
Insert cell
randomLetters = {
while (true) {
yield d3
.shuffle(alphabet.slice()) // create a shallow copy and shuffle it
.slice(Math.floor(Math.random() * 10) + 5) // take a random subset between 5 and 14 (inclusive) letters
.sort(d3.descending);
await Promises.delay(3000); // yield repeatedly after this time
}
}
Insert cell
chartDynamic = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, 33])
.attr("font-family", "sans-serif")
.attr("font-size", 16);

// create selection of text elements
let text = svg.selectAll("text");

// return the svg dom node with an update function attached to it
return Object.assign(svg.node(), {
// the update function takes letters as an argument and bind the new letters data to the svg.node()
update(letters) {
text = text
.data(letters)
.join("text")
.attr("x", (d, i) => i * 17)
.attr("y", 17)
.attr("dy", "0.35em")
.text((d) => d);
}
});
}
Insert cell
// use the generator randomLetters to udpate the chart
// will be undefined here, but the returned svg.node() in the above cell will show
chartDynamic.update(randomLetters)
Insert cell
chartDynamicIndexKey = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, 33])
.attr("font-family", "sans-serif")
.attr("font-size", 16);

let text = svg.selectAll("text");

return Object.assign(svg.node(), {
update(letters) {
text = text
.data(letters) // by default, d3 matches data to elements based on index
.join(
(enter) => // creates new text elements for any missing data points in letters
enter
.append("text")
.attr("y", 17)
.attr("dy", "0.35em")
.attr("fill", "orange")
.text((d) => d),
(update) => update.attr("fill", "black"), // updates the existing text elements to match the new data points
(exit) => exit.remove() // removes any text elements that are no longer needed
)
// everything after join is run on enter and update
.attr("x", (d, i) => i * 17);
}
});
}
// for join - key defines what should be checked between dataset items and corresponding svg elements
// without defining a key for join it uses the index for matching the elements to - "does the first element in svg elemnts match first element in data array?" etc. if it doesn't match it gets removed and a new one is added
Insert cell
chartDynamicIndexKey.update(randomLetters)
Insert cell
chartDynamicKey = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, 33])
.attr("font-family", "sans-serif")
.attr("font-size", 16);

let text = svg.selectAll("text");

return Object.assign(svg.node(), {
update(letters) {
text = text
.data(letters, (d) => d) // key function that determines how data points are matched to existing elements
.join(
(enter) =>
enter
.append("text")
.attr("y", 17)
.attr("dy", "0.35em")
.attr("fill", "orange")
.text((d) => d),
(update) => update.attr("fill", "black"),
(exit) => exit.remove()
)
// everything after join is run on enter and update
.attr("x", (d, i) => i * 17);
}
});
}
// for join - key defines what should be checked between dataset items and corresponding svg elements
// without defining a key for join it uses the index for matching the elements to - "does the first element in svg elemnts match first element in data array?" etc. if it doesn't match it gets removed and a new one is added
Insert cell
chartDynamicKey.update(randomLetters)
Insert cell
keyFunction1 = FileAttachment("key-function-1.png").image()
Insert cell
keyFunction2 = FileAttachment("key-function-2.png").image()
Insert cell
chartDynamicAnimation = {
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, 33])
.attr("font-family", "sans-serif")
.attr("font-size", 16);

let text = svg.selectAll("text");

return Object.assign(svg.node(), {
update(letters) {
const t = svg.transition().duration(750); // changes made to elements within the SVG during the transition will be animated
text = text
.data(letters, (d) => d)
.join(
(enter) =>
enter
.append("text")
.attr("y", -7) // new elements will appear above the existing elements, and will be animated downwards
.attr("x", (d, i) => i * 17)
.attr("dy", "0.35em")
.attr("fill", "green")
.text((d) => d),
(update) => update.attr("fill", "black"),
(exit) =>
exit
.attr("fill", "red") // color elements to be removed red
.call((text) => text.transition(t).attr("y", 41).remove()) // move them downwards out of the svg, and remove them
)
.call((text) => // after handling the data udpates, do the following with the text. called on enter and update selections
text
.transition(t)
.attr("y", 17) // final position of each enter and update element
.attr("x", (d, i) => i * 17)
);
}
});
}
Insert cell
chartDynamicAnimation.update(randomLetters)
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