Published
Edited
Nov 23, 2020
Importers
Insert cell
md`# Doube Axes Graph`
Insert cell
Insert cell
chart = {
const svg = d3.select(DOM.svg(width, height));

// Make our SVG responsive.
svg.attr('viewBox', `0 0 ${width} ${height}`);
svg.attr('preserveAspectRatio', 'xMidYMid meet');
svg.style('max-width', '100%').style('height', 'auto');

const showDGS = selectedType.includes('DGS');
const showStock = selectedType.includes('stock');

svg.append('g').call(title('DGS vs. Stock'));

// Draw the x and y axes.
svg.append('g').call(xAxis('Date'));
if (showDGS) {
svg.append('g').call(yAxis('DGS'));
}
if (showStock) {
svg.append('g').call(yAxis2('Stock'));
}

// Draw the grid lines.
svg.append('g').call(xGrid);
svg.append('g').call(yGrid);

// Total cases.

if (showDGS) {
svg
.append('g')
.selectAll('path')
.data(data1)
.join('path')
.attr('d', d => dsgLine(d))
.attr('class', 'total-cases');

svg
.append('g')
.selectAll('total-circle')
.data(data1)
.join('circle')
.style('pointer-events', 'none')
.style('fill', '#eb6f2d')
.attr('r', 3)
.attr('cx', d => xScale(d.DATE))
.attr('cy', d => yScale(d.DGS10));
}

// Daily cases.

if (showStock) {
svg
.append('g')
.selectAll('path')
.data(data2)
.join('path')
.attr('d', d => stockLine(d))
.attr('class', 'daily-cases');

svg
.append('g')
.selectAll('daily-circle')
.data(data2)
.join('circle')
.style('pointer-events', 'none')
.style('fill', '#54abb3')
.attr('r', 3)
.attr('cx', d => xScale(d.Date))
.attr('cy', d => yScale2(d.Close));
}

svg
.append('rect')
.style('fill', 'none')
//.style('opacity', 0.5)
.style('pointer-events', 'all')
.attr('width', width - margin.left - margin.right)
.attr('height', height - margin.top - margin.bottom)
.attr('transform', `translate(${margin.left},${margin.top})`)
.on('touchmove mousemove', mousemove);
//.on('touchend mouseout', mouseout)

const line = svg
.append('line')
.style('stroke', '#999')
.style('pointer-events', 'none')
.style('opacity', 0);

const circle1 = svg
.append('circle')
.style('stroke', '#eb6f2d')
.style('stroke-width', 4)
.style('fill', '#fff')
.attr('r', 5)
.style('opacity', 0);

const circle2 = svg
.append('circle')
.style('stroke', '#54abb3')
.style('stroke-width', 4)
.style('fill', '#fff')
.attr('r', 5)
.style('opacity', 0);

const dateText = svg
.append('text')
.attr('class', 'tooltip-text')
.attr('x', margin.left + 20)
.attr('y', margin.top + 20);

const totalCasesText = svg
.append('text')
.attr('class', 'tooltip-text')
.attr('x', margin.left + 20)
.attr('y', margin.top + 40);

const dailyCasesText = svg
.append('text')
.attr('class', 'tooltip-text')
.attr('x', margin.left + 20)
.attr('y', margin.top + (showDGS ? 60 : 40));

function mousemove() {
const d = bisect(d3.mouse(this)[0] + margin.left);
line
.attr('x1', xScale(d.DATE))
.attr('x2', xScale(d.DATE))
.attr('y1', margin.top)
.attr('y2', height - margin.bottom)
.style('opacity', 0.8);

circle1
.style('pointer-events', 'none')
.attr('cx', xScale(d.DATE))
.attr('cy', yScale(d.DGS10))
.style('opacity', showDGS ? 1 : 0);

circle2
.style('pointer-events', 'none')
.attr('cx', xScale(d.date))
.attr('cy', yScale2(d.Close))
.style('opacity', showStock ? 1 : 0);

dateText
.text('Date: ' + d3.timeFormat('%m/%d')(d.DATE))
.style('opacity', 1);

totalCasesText
.text('Total Cases: ' + d3.format(',')(d.DGS10))
.style('opacity', showDGS ? 1 : 0);

dailyCasesText
.text('New Cases: ' + d3.format(',')(d.Close))
.style('opacity', showStock ? 1 : 0);
}

return svg.node();
}
Insert cell
md`## Appendix`
Insert cell
height = 500
Insert cell
width = 960
Insert cell
margin = ({ top: 40, right: 80, bottom: 60, left: 80 })
Insert cell
xScale = {
const scale = d3.scaleUtc().range([margin.left, width - margin.right]);
scale.domain(d3.extent(data1.map(({ DATE }) => DATE))).nice();
return scale;
}
Insert cell
yScale = {
const scale = d3.scaleLinear().range([height - margin.bottom, margin.top]);
scale.domain([0, maxDGS]);
return scale;
}
Insert cell
getSmartTicks(maxDGS).endPoint
Insert cell
yScale2 = {
const scale = d3.scaleLinear().range([height - margin.bottom, margin.top]);
scale.domain([0, maxStock]);
return scale;
}
Insert cell
xAxis = label => g =>
g
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(
d3
.axisBottom(xScale)
.ticks()
.tickFormat(d3.timeFormat('%y/%m/%d'))
)
// Add label
.append('text')
.attr('class', 'axis-label')
.text(label)
.attr('x', margin.left + (width - margin.left - margin.right) / 2)
.attr('y', 50) // Relative to the x axis.
Insert cell
yAxis = label => g =>
g
.attr('class', 'total-cases-y-axis')
.attr('transform', `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale).ticks(10))
// Add label
.append('text')
.attr('class', 'axis-label')
.text(label)
.attr('transform', 'rotate(-90)')
.attr('x', -(margin.top + (height - margin.top - margin.bottom) / 2))
.attr('y', -60) // Relative to the y axis.
Insert cell
dsgTicksCount
Insert cell
yAxis2 = label => g =>
g
.attr('class', 'daily-cases-y-axis')
.attr('transform', `translate(${width - margin.right},0)`)
.call(d3.axisRight(yScale2).ticks(10))
// Add label
.append('text')
.attr('class', 'axis-label')
.text(label)
.attr('transform', 'rotate(-90)')
.attr('x', -(margin.top + (height - margin.top - margin.bottom) / 2))
.attr('y', 60) // Relative to the y axis.
Insert cell
xGrid = (g) => g
.attr('class', 'grid-lines')
.selectAll('line')
.data(xScale.ticks())
.join('line')
.attr('x1', d => xScale(d))
.attr('x2', d => xScale(d))
.attr('y1', margin.top)
.attr('y2', height - margin.bottom)
Insert cell
yGrid = g =>
g
.attr('class', 'grid-lines')
.selectAll('line')
.data(yScale.ticks(Math.max(dsgTicksCount, stockTicksCount)))
.join('line')
.attr('x1', margin.left)
.attr('x2', width - margin.right)
.attr('y1', d => yScale(d))
.attr('y2', d => yScale(d))
Insert cell
data1all = d3.csv(
"https://uw-info474.static.observableusercontent.com/files/902652e0bc1e36ba16f239e29131a116980df4beacac431468a44b74a2494040c3ddd33dd302e5cddfdeb3692706abbbff5594eec36e95e87e60473b69d34624?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27DGS10%2520(1)%2520(1).csv&Expires=1605441600000&Key-Pair-Id=APKAJCHFJLLLU4Y2WVSQ&Signature=ty4S1ABpsyx2xaaGi2uyQOskd-Yuz4mJBzApocGuUY9OzKQiCJcmskIiUWGfgLWX0g2GbSH7gChltWhE0RqF6MSvIaFTqBbdnKg7RRDc8mWq3NilvV0wbv63RE2RxUTES96THPdKpnN1sWsuFjI5Bv32cDGms9wfyyd8QgPHwa3EBCeCZww61E62F1aIh69vjt5MjTCRkdiVORMHainI9YI2DBl0oWqSNSxVylQ3IKlTWgCSkSmOA4ybcDz4gixHFjxgm86onB4BeqBpczPn-YkwR991OvEU72mYDIsl-vI-6atbPCf6ggQc7VQAgbbs2FeWwRLbmJEfVMlWPOCXwQ__",
d3.autoType
)
Insert cell
data1 = data1all.filter(d => d.DATE > new Date(2015, 10, 8))
Insert cell
data2 = data2all.filter(d => d.Date < new Date(2020, 9, 27))
Insert cell
data2all = d3.csv(
"https://uw-info474.static.observableusercontent.com/files/d3459d6d9755eb2aa5055456df00118bf52223c7aa64d44693e40a2bd487695bad61cb0e359bb7be9f68e265e5cacae671a0d9382c4951cd1822cd69516fdcac?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27%255EGSPC%2520(1).csv&Expires=1605441600000&Key-Pair-Id=APKAJCHFJLLLU4Y2WVSQ&Signature=lr5B5QZ5-aSwghSxsnnYO9oQwKpnMWvyKu4UKu7Njgkfd3x3VcllpFMlBXOKhe1Cz9TndnsLvkThuHRci0JFZz~a8-nXN9~TErprDeMMLXUW-jpBfsLknpq7fd-aRCsIa~P9IMz~47Xo0GkKukgsSgcDiOamDC72q~J~BQ86jRcAXgqbAtcHccbA868FRdp0IxS9V5vmE5UugiJCBObr3hu2hlQRYcp2gA1aVzY0CBJ-r91-G3pU8qsJoGKWe9~0o0JA5OvSCpAWsfEHnqV3IIXB1MJszJt~13EeIDRJlScAhLtc-BUFQ4Fc-xizN5up43gm5adEfQ~U0APrmHlG4g__",
d3.autoType
)
Insert cell
function getSmartTicks(val) {
//base step between nearby two ticks
var step = Math.pow(10, val.toString().length - 1);

//modify steps either: 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000...
if (val / step < 2) {
step = step / 5;
} else if (val / step < 5) {
step = step / 2;
}

//add one more step if the last tick value is the same as the max value
//if you don't want to add, remove "+1"
var slicesCount = Math.ceil((val + 1) / step);

return {
endPoint: slicesCount * step,
count: Math.min(10, slicesCount) //show max 10 ticks
};
}
Insert cell
bisect = {
const bisect = d3.bisector(d => d.date).left;
return mx => {
const data = data1;
const date = xScale.invert(mx);
const index = bisect(data, date, 1);
const a = data[index - 1];
const b = data[index];
if (!a) return b;
if (!b) return a;
return date - a.date > b.date - date ? b : a;
};
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
md`## Line Functions`
Insert cell
Insert cell
Insert cell
import { checkbox } from "@jashkenas/inputs"
Insert cell
title = label => g =>
g
.append('text')
.attr('class', 'title')
.text(label)
.attr('x', width / 2)
.attr('y', 20)
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