Published
Edited
Mar 11, 2021
8 stars
Insert cell
Insert cell
chart = {
const node = html`${style}<svg width="${width}" height="${height}">
${scaledPeaks.slice(0, 4).map(
d => svg`<g transform="translate(${x(d.summit.date)})">
<text y="1em">${formatDate(d.summit.date)}</text>
<text y="2em">${formatValue(d.summit.value)}</text>
</g>`
)}
<path d="${line(data)}"/>
<g id="peaks"/>
</svg>`;

const sel = d3.select(node).select("svg");

const p = sel
.select("g#peaks")
.selectAll("path")
.data(scaledPeaks)
.enter()
.append("path")
.attr("d", peak => line(peak.data))
.style("stroke", d => randColor());

async function drop() {
const t = p
.transition()
.duration(1000)
.delay((d, i) => 500 + i ** (1 / 3) * 500)
.attr(
"transform",
peak => `translate(${x(peak.summit.date)}, ${dx(peak.data.length)})`
)
.attr("d", peak =>
d3
.line()
.x(d => peak.x(d.date))
.y(d => peak.y(d.value))
.defined(d => d)(peak.data)
);
await t.end();
const tt = sel
.select("g#peaks")
.selectAll("path")
.transition()
.duration(1000)
.attr("transform", "translate(0,0)")
.attr("d", peak => line(peak.data));
await tt.end();
drop();
}

drop();

return node;
}
Insert cell
data = FileAttachment("btc-2021-03-09.csv").csv({typed: true})
Insert cell
peaks = getPeaks(data).filter(d => d.length > 3)
Insert cell
scaledPeaks = peaks.map(data => {
const radius = Math.floor(data.length / 2)
const summit = data[radius]
const scaledRadius = r(radius)
let x
if (data[0] === undefined) {
x = d3.scaleTime()
.domain([summit.date, data[data.length - 1].date])
.range([0, scaledRadius])
} else {
x = d3.scaleTime()
.domain([data[0].date, summit.date])
.range([-scaledRadius, 0])
}
const y = d3.scaleLinear()
.domain(d3.extent(data.filter(d => d).map(d => d.value)))
.range([scaledRadius * 0.4, -scaledRadius * 0.4])
return {data, radius, summit, x, y}
}).sort((a, b) => b.data.length - a.data.length)
Insert cell
getPeaks = data => {
const peaks = []
data.forEach((d, i, arr) => {
const peak = [d]
for (let radius = 1; radius < arr.length / 2; radius++) {
const next = arr[i + radius]
const prev = arr[i - radius]
const nextIsLower = !next || next.value < d.value
const prevIsLower = !prev || prev.value < d.value
if (nextIsLower && prevIsLower) {
peak.push(next)
peak.unshift(prev)
} else {
break;
}
}
peaks.push(peak)
})
return peaks
}
Insert cell
height = width * 0.7
Insert cell
h = height / 3.5
Insert cell
marginTop = 32
Insert cell
marginRight = 40
Insert cell
r = d3.scaleLinear()
.domain(d3.extent(peaks.map(d => Math.floor(d.length / 2))))
.range([1, width / 4])
Insert cell
x = d3.scaleTime()
.domain(d3.extent(data.map(d => d.date)))
.range([0, width - marginRight])
Insert cell
y = d3.scaleLinear()
.domain(d3.extent(data.map(d => d.value)))
.range([h, marginTop])
Insert cell
dx = d3.scaleLog()
.domain(d3.extent(peaks.map(d => d.length)))
.range([height, h + r.range()[1] / 3])
Insert cell
line = d3.line()
.x(d => x(d.date))
.y(d => y(d.value))
.defined(d => d)
Insert cell
randColor = () => `hsl(${360 * Math.random()}, 50%, 50%)`
Insert cell
style = html`<style>
path {
stroke: black;
fill: none;
}
text {
text-anchor: middle;
font-family: sans-serif;
font-size: 14px;
}
</style>`
Insert cell
formatDate = d3.timeFormat("%B %Y")
Insert cell
formatValue = d3.format("$,.0f")
Insert cell
d3 = require("d3")
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