Published
Edited
Apr 27, 2022
Insert cell
Insert cell
lower = 4
Insert cell
upper = 8
Insert cell
viewof plot1 = {

let data = [{airline: "a", values: []}, {airline: "b", values: []}, {airline: "c", values: []}]
rawData.forEach(function(d) {
data[["a", "b", "c"].indexOf(d.airline)].values.push({date: d.date, price: d.price})
});
let svgPlotContainer = d3.create("svg")
.attr("width", plotVars.plotWidth)
.attr("height", plotVars.plotHeight)
.property("value", []);
svgPlotContainer.append("g")
.attr('class', 'grid')
.call(xAxis)

svgPlotContainer.append("g")
.attr('class', 'grid')
.call(yAxis);

const legend = svgPlotContainer.append("g")
legend.selectAll("legend")
.data([not_included])
.join('rect')
.attr('x', 10)
.attr('y', plotVars.plotHeight - margin.bottom + 12 )
.attr('width', 30)
.attr('height', 20)
.attr('stroke', 'black')
.attr('stroke-width', '2')
.attr('fill', 'green')
.attr('fill-opacity', 0.05)
.attr('stroke', 'green')
.attr('transform', (d,i) => "translate(0," + i * 25 + ")")

legend.selectAll("g")
.data([not_included])
.join('text')
.attr('x', 25)
.attr('y', plotVars.plotHeight - margin.bottom + 10 + 17)
.attr('transform', (d,i) => "translate(0," + i * 25 + ")")
.attr("font-size", "1em")
.attr("fill", "green")
.text(function(d){ return d;})
.attr("text-anchor", "middle")
.attr("font-family", "Montserrat")
svgPlotContainer.append("g")
.selectAll("path")
.data(data)
.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", 'black'); // Set a color for the line (The maps for color is in 'color')

svgPlotContainer.append("g")
.selectAll("line")
.data([lower,upper])
.join("svg:line")
.attr("x1", margin.left)
.attr("x2", plotVars.plotWidth - margin.right)
.attr("y1", d => yScale(d))
.attr("y2", d => yScale(d))
.attr('stroke', 'green')
.attr('stroke-width', 2)

svgPlotContainer.selectAll("area")
.data([{start: lower, end: upper}])
.join('rect')
.attr('x', margin.left)
.attr('y', d => yScale(d.end))
.attr('width', plotVars.plotWidth - margin.right - margin.left)
.attr('height', d => yScale(d.start) - yScale(d.end))
.attr('fill', 'green')
.attr('fill-opacity', 0.05)
.attr('transform', (d,i) => "translate(0," + i * 25 + ")")
let brushContainer = svgPlotContainer.append("g");

const brush = d3.brush()
.on("start brush end", brushed(svgPlotContainer.selectAll("path.data-line"), svgPlotContainer))
.extent([[margin.left, margin.top], [plotVars.plotWidth - margin.right, plotVars.plotHeight - margin.top]]); // Sets the brushable area [[xMin, yMin], [xMax, yMax]]
brushContainer.call(brush);
return svgPlotContainer.node();
}
Insert cell
not_included = 28
Insert cell
plot1
Insert cell
function brushed(lines, svg) {
return function({selection}) {
let value = [];
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("stroke", "black");
} else {
value = lines.style("stroke", "black") // Start by setting all lines black
.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("stroke", "gold")
.data()
}
} else {
// Nothing has been selected yet
lines.style("stroke", "black");
}
svg.property("value", value).dispatch("input");
}
}
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
margin = ({top: 50, right: 50, bottom: 50, left: 50})
Insert cell
rawData
Insert cell
yScale = d3.scaleLinear()
.domain([0, d3.max(rawData, d => d.price)]) // Try using d3.max(). We want the max of the price
.range([plotVars.plotHeight - margin.bottom, margin.top])
Insert cell
xScale = d3.scaleLinear()
.domain(d3.extent(rawData, d => d.date))
.range([margin.left, plotVars.plotWidth - margin.right])
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
unique_dates = [...new Set(rawData.map(item => item.date))];
Insert cell
unique_labels = [...new Set(rawData.map(item => item.display))];
Insert cell
unique_dates
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${plotVars.plotHeight - margin.bottom})`)
.call(d3.axisBottom(xScale)
//.ticks(unique_dates.length-1)
.tickValues(unique_dates)
.tickSize(-plotVars.plotHeight+margin.top+margin.bottom)
.tickFormat((d,i) => unique_labels[i])
)
.selectAll("text")
.attr('fill', 'black')
.attr('font-size', "1.5em")
.attr("x", (d,i) => i === 0 ? "2.2em" : "1em")
.attr("transform", "rotate(10)");
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale)
.ticks(5)
.tickSize(-plotVars.plotWidth+margin.left+margin.right)
)
//.call(g => g.select(".domain").remove())
.call(g => g.select(".tick:last-of-type text").clone()
.attr("x", 4)
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text(rawData.price))
.selectAll("text")
.attr('fill', 'black')
.attr('font-size', "1.8em")
Insert cell
Insert cell
<style>
.grid .tick {
stroke: #EFEFEF;
}
.grid path {
stroke: #EFEFEF;
}

.grid .tick line {
stroke: #EFEFEF;
}

.grid .tick text {
fill: black;
stroke: none;
}
</style>
Insert cell
Insert cell
d3.range(rawData, d => d.price)
Insert cell
rawData.forEach(function(d) {
data[["a", "b", "c"].indexOf(d.airline)].values.push({date: d.date, price: d.price})
});
Insert cell
Insert cell
Insert cell
d3 = require('d3@6')
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