Published
Edited
Jan 7, 2021
Importers
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
plot(
{
sin: now => -Math.sin(now / 400),
cos: now => -Math.cos(now / 200),
rand: now => Math.random() - .5
},
{
yRange: [-2, 2],
yPadding: 0,
windowMillis: 5000,
delayMillis: 1000
}
)
Insert cell
defaultOptions = ({
// windowMillis: the displayed time window in milliseconds
windowMillis: 10 * 1000,
// delayMillis: the lag between the current time and the latest displayed value
delayMillis: 1 * 1000,
// width: plot width in pixels
width: width,
// height: plot height in pixels
height: 256,
// yRange: min and max values for the vertical scale
// automatically determined by default
yRange: undefined,
// yPadding: extra space between the bottom/top of yRange and the graph top
yPadding: undefined,
// margins outside the plot
margins: { top: 10, bottom: 20, left: 25, right: 1 }
})
Insert cell
Insert cell
function* plot(fns, userOptions = defaultOptions) {
const options = _.merge({}, defaultOptions, userOptions);
const buffer = new Buffer(options);
const plotter = new Plotter(options);
while (true) {
const { data, minY, maxY } = buffer;
const now = new Date().getTime();

Object.keys(fns).forEach(id => {
const val = fns[id](now);
buffer.push(id, now, val);
});

yield plotter.draw(buffer, now);
}
}
Insert cell
class Buffer {
constructor(options) {
this.options = options;
this.dataById = {};
this.minY = +Infinity;
this.maxY = -Infinity;
}

push(id, x, y) {
const { dataById, options, minY, maxY } = this;
const { windowMillis, delayMillis } = options;
dataById[id] = dataById[id] || [];
const data = dataById[id];
data.push([x, y]);
this.minY = Math.min(y, minY);
this.maxY = Math.max(y, maxY);
while (data.length > ((windowMillis + delayMillis) / 1000) * 60) {
const [ignored, removedY] = data.shift();
if (removedY === this.minY && !this.yRange) {
this.minY = data.reduce((acc, cur) => Math.min(acc, cur[1]), +Infinity);
}
if (removedY === this.maxY && !this.yRange) {
this.maxY = data.reduce((acc, cur) => Math.max(acc, cur[1]), -Infinity);
}
}
}

yRange() {
return [this.minY, this.maxY];
}
}
Insert cell
class Plotter {
constructor(options) {
this.options = options;
}

draw(buffer, now) {
const {
width,
height,
windowMillis,
delayMillis,
yRange,
margins,
yPadding
} = this.options;
const svg = d3.select(DOM.svg(width, height));
const xRange = [now - (windowMillis + delayMillis), now - delayMillis];
const xScale = d3.scaleTime(xRange, [margins.left, width - margins.right]);
const [minY, maxY] = yRange || buffer.yRange();
const padding = isNaN(yPadding) ? (maxY - minY) / 8 : yPadding;
const yScale = d3.scaleLinear(
[minY - padding, maxY + padding],
[height - margins.bottom, margins.top]
);
const plotLine = d3
.line()
.x(d => xScale(d[0]))
.y(d => yScale(d[1]));

const rightAxis = d3
.axisRight(yScale)
.tickSize(width - (margins.left + margins.right))
.ticks(10, "+f");
const bottomAxis = d3
.axisTop(xScale)
.tickSize(height - (margins.top + margins.bottom));
const dashedAxis = g =>
g
.selectAll(".tick line")
.attr("stroke-opacity", 0.5)
.attr("stroke-dasharray", "2,2");
svg
.append('clipPath')
.attr('id', 'chart-area')
.append('rect')
.attr('x', margins.left - 1)
.attr('y', margins.top)
.attr('width', width - (margins.left + margins.right))
.attr('height', height - (margins.top + margins.bottom));
svg
.append("g")
.attr("transform", `translate(0,${height - margins.bottom})`)
.call(bottomAxis)
.call(dashedAxis);
svg
.append("g")
.attr("transform", `translate(${margins.left},0)`)
.call(rightAxis)
.call(dashedAxis)
.call(g => g.selectAll(".tick text").attr("x", -25));
Object.keys(buffer.dataById).forEach((id, idx) => {
const data = buffer.dataById[id];
svg
.append('path')
.datum(data)
.attr("clip-path", "url(#chart-area)")
.attr('fill', 'none')
.attr('stroke', d3.schemeTableau10[idx % 10])
.attr('stroke-width', 2)
.attr("transform", "translate(0,0)")
.attr('d', plotLine);
});
return svg.node();
}
}
Insert cell
d3 = require('d3')
Insert cell
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