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

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