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

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