Published
Edited
Jan 5, 2021
2 stars
Insert cell
Insert cell
timeline = {
const defaultData = {
"2019-07-31T00:00": 19924354,
"2019-08-01T00:00": 19924354,
"2019-08-02T00:00": 19932514,
"2019-08-04T00:00": 19950795,
"2019-08-06T00:00": 19966611,
"2019-08-10T00:00": 20004292,
"2019-08-12T00:00": 20028154,
"2019-08-14T00:00": 20051424,
"2019-08-31T00:00": 20259946,
"2019-09-08T00:00": 20353998
}
return Object.entries(defaultData)
.map(([key, value], i) => ({key, value, dateKey: new Date(key), index: i}))
.sort((a,b) => d3.ascending(a.key, b.key))
}
Insert cell
width = 700

Insert cell
height = 700
Insert cell
marginX = 100
Insert cell
marginY = 100
Insert cell
viewof voronoiVisible = checkbox({
description: "",
options: [{ value: "toggle", label: "show voronoi vertices" }],
value: "toggle"
})
Insert cell
{
const svg = d3.create('svg');
svg.attr('width', width).attr('height', height);
const stage = svg.append('g');
stage.attr('transform', `translate(${marginX},${marginY})`);
const keys = timeline.map(t => t.dateKey);
const values = timeline.map(t => t.value);
const xMax = Math.max(...keys);
const xMin = Math.min(...keys);
const yMax = Math.max(...values);
const yMin = Math.min(...values);
const xScaler = d3
.scaleLinear()
.domain([xMin, xMax])
.range([0, width - marginX * 2]);
const yScaler = d3
.scaleLinear()
.domain([yMin, yMax])
.range([height - marginY * 2, 0]);

const xAxis = stage
.append('g')
.attr('transform', `translate(0,${height - marginY * 2})`)
.classed('x-axis', true)
.call(
d3
.axisBottom(xScaler)
.tickValues(keys)
.tickFormat(d3.timeFormat('%b %d'))
);
const yAxis = stage
.append('g')
.classed('y-axis', true)
.call(
d3
.axisLeft(yScaler)
.tickValues(values)
.tickFormat(d3.format('~s'))
);

const lineDraw = d3
.line()
.x(d => xScaler(d.dateKey))
.y(d => yScaler(d.value));

const line = stage
.append('path')
.datum(timeline)
.classed('line-path', true)
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1.5)
.attr('d', lineDraw);

const points = stage
.selectAll('.point')
.data(timeline)
.join('circle')
.attr('class', d => `point-${d.index}`)
.classed('point', true)
.attr('fill', 'black')
.attr('cx', 4)
.attr('cy', 4)
.attr('r', 4)
.attr(
'transform',
d => `translate(${xScaler(d.dateKey) - 4},${yScaler(d.value) - 4})`
);

let infoHover = null;

const onMouseOver = (d, i, e) => {
console.log('mouseover', d);
svg.selectAll(`.point-${d.index}`).classed('hover', true);
if (!infoHover) infoHover = stage.append('text');
infoHover
.text(`${d3.timeFormat('%b %d')(d.dateKey)}-${d3.format('~s')(d.value)}`)
.attr(
'transform',
`translate(${xScaler(d.dateKey) - 4},${yScaler(d.value) - 4})`
);
};

const onMouseOut = d => {
// console.log('mouseout',d)
svg.selectAll(`.point-${d.index}`).classed('hover', false);
if (infoHover) {
infoHover.remove();
infoHover = null;
}
};

const delaunay = D3Delaunay.Delaunay.from(
timeline,
d => xScaler(d.dateKey),
d => yScaler(d.value)
); // data, fx, fy
const voronoi = delaunay.voronoi([
0,
0,
width - marginX * 2,
height - marginY * 2
]);
// console.log(voronoi.render())

const voronoiPath = stage
.append('g')
.selectAll('voronoi-path')
.data(timeline)
.join('path')
.attr('d', (d, i) => voronoi.renderCell(i))
.classed('voronoi-path', true)
.attr('stroke-width', 1.0)
.attr('stroke', voronoiVisible ? 'pink' : 'transparent')
.attr('fill', 'transparent')
.on('mouseover', onMouseOver);

return svg.node();
}
Insert cell
d3 = require('d3@v5')
Insert cell
D3Delaunay = require('d3-delaunay');
Insert cell
import {checkbox} from "@jashkenas/inputs"
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