Published
Edited
Oct 19, 2020
5 stars
Insert cell
Insert cell
Insert cell
airlineRawData = d3.csv("https://magrawala.github.io/cs448b-fa20/assets/docs/airlines.csv", function(d) {
return {
airline: d.airline,
date: d3.timeParse('%m/%d/%y')(d.date), // We need to parse the date string into a Date object using d3.timeParse. Here, the string looks like 10/15/19 (%m/%d/%y).
price: +d.price
};
}).then(function(airlineRawData) {
//Outside of Observable notebooks you would put all code to draw the graph here.
return airlineRawData;
});
Insert cell
printTable(airlineRawData.slice(0, 10, 5))
Insert cell
Insert cell
airlineData = [{airline: "AAL", values: []}, {airline: "UAL", values: []}, {airline: "DAL", values: []}]
Insert cell
Insert cell
airlineRawData.forEach(function(d) {
airlineData[["AAL", "UAL", "DAL"].indexOf(d.airline)].values.push({date: d.date, price: d.price})
});
Insert cell
Insert cell
d3.extent(airlineRawData, d => d.date);
Insert cell
d3.extent(airlineData[0].values, d => d.date)
Insert cell
Insert cell
Insert cell
color = ({"AAL": "gray", "UAL": "steelblue", "DAL": "firebrick"});
Insert cell
Insert cell
Insert cell
Insert cell
plotVars = ({
plotWidth: 600, // Width of plot region
plotHeight: 300, // Height of plot region
plotMargin: 50 // Margin space for axes and their labels
});
Insert cell
Insert cell
Insert cell
xScale = d3.scaleTime()
.domain(d3.extent(airlineRawData, d => d.date)) // Try using d3.extent(), which gives [min, max]. We want to know the date ranges among the dates
.range([0, 600]);
Insert cell
yScale = d3.scaleLinear()
.domain([0, d3.max(airlineRawData, d => d.price)]) // Try using d3.max(). We want the max of the price
.range([300, 0]); // Don't forget to flip as the origin of the SVG is at top, left
Insert cell
Insert cell
Insert cell
{
let svgPlotContainer = d3.create("svg")
.attr("width", 250)
.attr("height", 250)
.style("background-color", "whitesmoke"); // Color container bg to see its extent
svgPlotContainer.append("path")
.attr("d", "M50,10L75,200L225,100")
.attr("fill", "none") // Do not fill the area defined by the path (Try erasing this if you are curious)
.attr("stroke", "steelblue"); // Set a color for the line
return svgPlotContainer.node();
}
Insert cell
Insert cell
lineGenerator = d3.line()
.x(d => xScale(d.date)) // date should go on the x-axis
.y(d => yScale(d.price)); // price should go on the y-axis
Insert cell
lineGenerator(airlineData[0].values)
Insert cell
Insert cell
function drawLines(airlineData, plotContainer) {
plotContainer.append("g")
.selectAll("path")
.data(airlineData)
.join("path")
.attr("class", "data-line") // Class name to be able to access the lines later
.attr("d", d => lineGenerator(d.values)) // Use lineGenerator on d.values
.attr("fill", "none") // Do not fill the area defined by the path
.attr("stroke", d => color[d.airline]); // Set a color for the line (The maps for color is in 'color')
}
Insert cell
{
let svgPlotContainer = d3.create("svg")
.attr("width", plotVars.plotWidth)
.attr("height", plotVars.plotHeight)
.style("background-color", "whitesmoke"); // Color container bg to see its extent
drawLines(airlineData, svgPlotContainer); // Uncomment this to see the result
return svgPlotContainer.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
let svgPlotContainer = d3.create("svg")
.attr("width", 250)
.attr("height", 250)
.style("background-color", "whitesmoke"); // Color container bg to see its extent
const brush = d3.brush();
svgPlotContainer.call(brush); // Attach the brush to svgPlotContainer
return svgPlotContainer.node();
}
Insert cell
Insert cell
function brushed(lines) {
return function({selection}) {
if (selection) {
const [[xMin, yMin], [xMax, yMax]] = selection; // Unpack the bounding box of the selection
if (xMin === xMax && yMin === yMax) {
// The selection box is empty
lines.style("opacity", 1);
} else {
lines.style("opacity", 0.2) // Start by setting all opacity to 0.2
.filter(function (d) {
var internalPoints = d.values.filter(
v => (xMin <= xScale(v.date)) && (xScale(v.date) <= xMax));
function findIntersection(x) {
// Find the lower & upper bound points.
var lower = d.values.filter(v => x >= xScale(v.date))
.reduce((a, b) => a.date >= b.date ? a : b)
var upper = d.values.filter(v => x <= xScale(v.date))
.reduce((a, b) => a.date >= b.date ? b : a)
var lowerX = xScale(lower.date)
var lowerY = yScale(lower.price)
var upperX = xScale(upper.date)
var upperY = yScale(upper.price)
// Find the parameters of the line.
var a = (upperY - lowerY) / (upperX - lowerX)
var b = upperY - a * upperX
// Find the y-coord of the line at this x-coord.
return a * x + b
}
var xMinYInt = findIntersection(xMin)
var xMaxYInt = findIntersection(xMax)
var internalPrices = internalPoints.map(v => yScale(v.price));
internalPrices.push(xMinYInt)
internalPrices.push(xMaxYInt)
var dyMin = Math.min(...internalPrices);
var dyMax = Math.max(...internalPrices);
return dyMin >= yMin && dyMax <= yMax
})
.style("opacity", 1);
}
} else {
// Nothing has been selected yet
lines.style("opacity", 1);
}
}
}
Insert cell
airlineData[0].values[5]
Insert cell
{
//airlineData[0].values[0]
function roundToDay(date) {
const p = 60 * 60 * 1000 * 24; // milliseconds in a day.
return new Date(Math.round(date.getTime() / p ) * p);
}
return roundToDay(xScale.invert(500))
}
Insert cell
Insert cell
Insert cell
function addBrush(brushContainer, lines) {
const brush = d3.brush()
.on("start brush end", brushed(lines))
.extent([[0, 0], [plotVars.plotWidth, plotVars.plotHeight]]); // Sets the brushable area [[xMin, yMin], [xMax, yMax]]
brushContainer.call(brush);
}
Insert cell
Insert cell
{
let svgPlotContainer = d3.create("svg")
.attr("width", plotVars.plotWidth)
.attr("height", plotVars.plotHeight)
.style("background-color", "whitesmoke"); // Color container bg to see its extent
drawLines(airlineData, svgPlotContainer);
let brushContainer = svgPlotContainer.append("g");
addBrush(brushContainer, svgPlotContainer.selectAll("path.data-line"));

return svgPlotContainer.node();
}
Insert cell
{
return (airlineData[0].values
.filter(d => 250 <= xScale(d.date) && xScale(d.date) <= plotVars.plotWidth)
.map(d => d.date)).length;
}
Insert cell
{
var margin = {
top: 10,
right: 10,
bottom: 10,
left: 10
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;

var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");

svg.append("rect")
.attr("class", "grid-background")
.attr("width", width)
.attr("height", height);

// We initially generate a SVG group to keep our brushes' DOM elements in:
var gBrushes = svg.append('g')
.attr("class", "brushes");

// We also keep the actual d3-brush functions and their IDs in a list:
var brushes = [];

/* CREATE NEW BRUSH
*
* This creates a new brush. A brush is both a function (in our array) and a set of predefined DOM elements
* Brushes also have selections. While the selection are empty (i.e. a suer hasn't yet dragged)
* the brushes are invisible. We will add an initial brush when this viz starts. (see end of file)
* Now imagine the user clicked, moved the mouse, and let go. They just gave a selection to the initial brush.
* We now want to create a new brush.
* However, imagine the user had simply dragged an existing brush--in that case we would not want to create a new one.
* We will use the selection of a brush in brushend() to differentiate these cases.
*/
function newBrush() {
var brush = d3.brush()
.on("start", brushstart)
.on("brush", brushed)
.on("end", brushend);

brushes.push({id: brushes.length, brush: brush});

function brushstart() {
// your stuff here
};

function brushed() {
if (d3.event.sourceEvent.shiftKey)
}

function brushend() {

// Figure out if our latest brush has a selection
var lastBrushID = brushes[brushes.length - 1].id;
var lastBrush = document.getElementById('brush-' + lastBrushID);
var selection = d3.brushSelection(lastBrush);

// If it does, that means we need another one
if (selection && selection[0] !== selection[1]) {
newBrush();
}

// Always draw brushes
drawBrushes();
}
}

function drawBrushes() {

var brushSelection = gBrushes
.selectAll('.brush')
.data(brushes, function (d){return d.id});

// Set up new brushes
brushSelection.enter()
.insert("g", '.brush')
.attr('class', 'brush')
.attr('id', function(brush){ return "brush-" + brush.id; })
.each(function(brushObject) {
//call the brush
brushObject.brush(d3.select(this));
});

/* REMOVE POINTER EVENTS ON BRUSH OVERLAYS
*
* This part is abbit tricky and requires knowledge of how brushes are implemented.
* They register pointer events on a .overlay rectangle within them.
* For existing brushes, make sure we disable their pointer events on their overlay.
* This frees the overlay for the most current (as of yet with an empty selection) brush to listen for click and drag events
* The moving and resizing is done with other parts of the brush, so that will still work.
*/
brushSelection
.each(function (brushObject){
d3.select(this)
.attr('class', 'brush')
.selectAll('.overlay')
.style('pointer-events', function() {
var brush = brushObject.brush;
if (brushObject.id === brushes.length-1 && brush !== undefined) {
return 'all';
} else {
return 'none';
}
});
})

brushSelection.exit()
.remove();
}

newBrush();
drawBrushes();
return svg.node();
}
Insert cell
Insert cell
Insert cell
{
let svgPlotContainer = d3.create("svg")
.attr("width", 250)
.attr("height", 250);
const brush = d3.brush();
svgPlotContainer.call(brush); // Attach the brush to svgPlotContainer
// Color overlay rectangle with firebrick
svgPlotContainer.selectAll("rect.overlay")
.style("fill", "firebrick")
.style("opacity", 0.5);
// Color the selection rectangle
svgPlotContainer.selectAll("rect.selection")
.style("fill", "steelblue")
.style("opacity", 0.5);
// Color the handles
svgPlotContainer.selectAll("rect.handle")
.style("fill", "gray")
.style("opacity", 0.5);
return svgPlotContainer.node();
}
Insert cell
Insert cell
Insert cell
{
let svgPlotContainer = d3.create("svg")
.attr("width", 250)
.attr("height", 250)
.style("background-color", "whitesmoke"); // Color container bg to see its extent
const brush = d3.brush()
.on("end", function() {
svgPlotContainer.selectAll("rect.selection")
.style("fill", null)
// .style("opacity", 0.5);
}); // Remove the overlay when the selection is completed.
svgPlotContainer.call(brush); // Attach the brush to svgPlotContainer
return svgPlotContainer.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function addMoveCallback(rect) {
function moved(event) {
rect.attr("x", event.dx + rect.attr("x")) // Shift the x value by event.dx
.attr("y", event.dy + rect.attr("y")); // Shift the y value by event.dy
}

// Attach callback to rect
rect.call(d3.drag()
.on("drag", moved)); // Use our 'moved' function
}
Insert cell
Insert cell
{
let svgPlotContainer = d3.create("svg")
.attr("width", 250)
.attr("height", 250)
.style("background-color", "whitesmoke"); // Color container bg to see its extent
let rect = svgPlotContainer.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("width", 50)
.attr("height", 50)
.style("fill", "firebrick")
.style("cursor", "all-scroll"); // Change the cursor to show that the rect can be moved.
addMoveCallback(rect);
return svgPlotContainer.node();
}
Insert cell
Insert cell
function addResizeCallback(rect, controlRect) {
let initialMouseY;
let initialRectY;
let initialRectHeight;
// Obtain the initial rectangle and mouse status
function resizeStarted(event) {
initialMouseY = event.y;
initialRectY = +rect.attr("y");
initialRectHeight = +rect.attr("height");
}
function resized(event) {
let minY = /* Fill in */; // Compute the minimum value of y of the rect
let maxY = /* Fill in */; // Compute the maximum value of y of the rect
rect.attr("y", /* Fill in */) // set the new y of the rect
.attr("height", /* Fill in */); // set the new height of the rect
controlRect.attr("y", initialRectY + event.y - initialRectY - 3); // Set the new y of the control
}
// Always keep the top control on top
function resizeEnded(event) {
let minY = Math.min(initialRectY + event.y - initialMouseY, initialRectY + initialRectHeight);
controlRect.attr("y", minY - 3);
}

// Attach callback to controlRect
controlRect.call(d3.drag()
.on("start", resizeStarted)
.on("drag", resized)
.on("end", resizeEnded));
}
Insert cell
Insert cell
function addMoveCallbackWithControl(rect, controlRect) {
function moved(event) {
rect.attr("x", +rect.attr("x") + event.dx) // Shift the x value by event.dx
.attr("y", +rect.attr("y") + event.dy); // Shift the y value by event.dy
controlRect.attr("x", /* Fill in */) // Shift the x value by event.dx
.attr("y", /* Fill in */); // Shift the y value by event.dy
}

// Attach callback to rect
rect.call(d3.drag()
.on("drag", moved));
}
Insert cell
Insert cell
{
let svgPlotContainer = d3.create("svg")
.attr("width", 250)
.attr("height", 250)
.style("background-color", "whitesmoke"); // Color container bg to see its extent
let rect = svgPlotContainer.append("rect")
.attr("x", 100)
.attr("y", 100)
.attr("width", 50)
.attr("height", 50)
.style("fill", "firebrick")
.style("cursor", "all-scroll");
let controlRect = svgPlotContainer.append("rect")
.attr("x", 100)
.attr("y", 100 - 3)
.attr("width", 50)
.attr("height", 6) // Set the thickness to 6.
.style("fill", "black")
.style("cursor", "ns-resize");
addMoveCallbackWithControl(rect, controlRect);
addResizeCallback(rect, controlRect);
return svgPlotContainer.node();
}
Insert cell
Insert cell
Insert cell
d3 = require('d3@6')
Insert cell
import {printTable} from '@uwdata/data-utilities'
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