Public
Edited
Oct 1, 2021
Fork of Simple D3
3 stars
Insert cell
Insert cell
{
// create a contager SVG so we can use `rect` tags and such
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)

// how far down should the x axis show up?
const axisPosition = 220

// which industry do we want to narrow in on (for now)
const selectedIndustry = "Tech, Apps & Platforms"

// add a black background behind the entire thing (important to do this *outside* of the `chart` g containter
svg.append("g")
.attr("transform", "translate(0,0)")
.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "black")

// draw the axis using some D3 defaults
svg.append("g")
.attr("transform", `translate(0,${axisPosition})`)
.attr("color", "white")
.call(d3.axisBottom(yearScale));

// add a label below the axis
svg.append("g")
.attr("transform", `translate(50,${axisPosition+30})`)
.append("text")
.attr("font-size", 16)
.attr("font-family", "Arial")
.attr("fill", "white")
.text(selectedIndustry)
// setup a container to hold the chart itself, so our calls to `select` only work on things inside of this container
const chart = svg.append("g")
.attr("transform", "translate(0,0)")

// add rectangles for the size of each data breach
chart.selectAll("rect")
.data(data.filter(d => d.records != null).filter(d => d.industry == selectedIndustry))
.join("rect")
.attr("transform", d => `rotate(-45 ${yearScale(d.date)} ${axisPosition})`)
.attr("x", d => yearScale(d.date))
.attr("y", d => axisPosition - recordsScale(d.records))
.attr("width", d => recordsScale(d.records))
.attr("height", d => recordsScale(d.records))
.attr("fill", "rgb(20,255,20,0.3)")
.attr("stroke", "lightgreen")

// add dots at the point of each data breach, in the corner
chart.selectAll("circle")
.data(data.filter(d => d.industry == selectedIndustry))
.join("circle")
.attr("cx", d => yearScale(d.date))
.attr("cy", axisPosition)
.attr("r", 2)
.attr("fill", "white")

// tell Observable to draw the whole thing we just made
return svg.node()
}
Insert cell
// a function that accepts the number of records stoeln and returns a size we can use for the rectangle to draw
recordsScale = d3.scaleLinear()
.domain(d3.extent(data.map(d => d.records)))
.range([10,140]) // eyeballed from looking at the original Bloomberg graphic
Insert cell
// a function that accepts a date and turns it into a pixel distance along the X axis
yearScale = d3.scaleTime()
.domain(d3.extent(data.map(d => d.date)))
.range([50, width-50]) // leave 50 pixels on either side for padding
Insert cell
height = 300
Insert cell
// now we need to clean up the data
data = originalData.map(d => ({
...d,
date: new Date(d.date), // turn the date from a string into a Date type of variable
records: cleanRecords(d.records) // turn the records count from a string into a number
}))
.filter(d => d.records != null) // remove any rows that didn't have a record count
.filter(d => !isNaN(d.date)) // remove any rows that didn't have a valid date
Insert cell
// turn a string into a number for us
cleanRecords = recordAsString => {
if (recordAsString.endsWith("M")) {
return recordAsString.substring(0, recordAsString.length-1) * 1000000
} else if (recordAsString.endsWith("B")) {
return recordAsString.substring(0, recordAsString.length-1) * 1000000000
}
return null
}
Insert cell
originalData = FileAttachment("data-breaches.csv").csv()
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