Published
Edited
Jan 16, 2020
1 star
Insert cell
md`# d3 circular buffer

A circular buffer. Each added cell takes the value of the previous, plus an RNG algorithm. When the queue is full, head and tail indexes shift around.

You can find the article this was used in [here.](https://xy2.dev/article/re-skgba/re-skgba.html)

Read about [the data join](https://bost.ocks.org/mike/selection/). It's the most important part of d3!

This chart uses [reusable charts](https://bost.ocks.org/mike/chart/) with closures.`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class CircularBuffer {
constructor(size, array) {
// Represents the maximum size of the buffer,
// not its actual size.
this.size = size;
this.head = 0;
this.tail = 0;
this.full = false;
this.array = [];
if (array !== undefined) {
array.forEach(
e => this.enqueue(e)
);
}
}
empty() {
return (! this.full && this.head == this.tail);
}
advance() {
if (this.full) {
this.tail = (this.tail + 1) % this.size
}
this.head = (this.head + 1) % this.size
this.full = (this.head == this.tail)
}
enqueue(value) {
this.array[this.head] = value
this.advance()
}
get() {
if (this.empty()) {
console.log("get: queue is empty")
return;
}
return this.array[this.tail];
}
get_head() {
if (this.empty()) {
console.log("get: queue is empty")
return;
}
var head = mod(this.head - 1, this.size) // See above
return this.array[head];
}
consume() {
var newest = this.get_head()
var advanced = RNG(newest)
this.enqueue(advanced)
return newest;
}
}
Insert cell
// Closure for modularity and configuration.
// See t.
function chart(selection) {
function toHex(n) {
if (n === -1) return "";
return "0x" + n.toString(16);
};
var colorScale = d3.scaleSequential()
.domain([0x0, 0xffffffff])
.interpolator(d3.interpolateOranges);

// Compute the position of each group on the pie:
var pie = d3.pie()
.startAngle(-90 * Math.PI/180) // Angle it like a circle!
.endAngle(-90 * Math.PI/180 + 2*Math.PI)
.value(1) // Make the slices constant size.
function interpol(a, b) {
return d3.interpolateNumber(a, b);
}
// Transition time, in ms
var transitionTime = 750;
function my() {
selection.each(function(data) {
// Dynamic size SVG, responsive.
// selection.node().getBoundingClientRect gets the bounding rectangle
// of the parent element. I've put my graph inside a div,
// but anything works. The size will be determined by the element's initial
// width as reported by getBoundingClientRect.
//var bbox = selection.node().getBoundingClientRect()
// Hardcoded because iframe..
var bbox = {width: 600}
var width = 3/4 * bbox.width;
// Make height the same as width. As such, only the width of the page
// matters for determining size.
var height = width;
var radius = Math.min(width, height) / 2;
var innerRadius = radius / 2;
//
var svgElement = d3.select(this).selectAll("svg")
// Create the SVG if it doens't exist (enter selection)
svgElement
// Create a data join with empty data,
// see https://github.com/d3/d3-selection/issues/91 for rationale
.data([null])
.enter()
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Main g element of svg (misleading named svg)
var svg = d3.select(this).select("g")
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(radius)
.padAngle(2* Math.PI/180)
/* Why? The pie is not fixed size. We are trying to represent
* an array where each element is fixed size, but the pie
* will extend to an entire circle by default.
* Adjusting start and end angle necessary is needed.
* This function mutates pie.
*/
function updateStartEndAngles(data) {
// Note that we angle the pie like a circle, to make
// the invertArcIfBottom() calculations easier.
var startAngle = -90 * Math.PI/180
// How much area does a single cell cover?
// 2*pi is the radius of a circle.
// We calculate against the buffer's max size.
var step = 2*Math.PI / data.size
// How many cells are there? That's going to be the end angle,
// relative to the starting one.
// If data.array.length == data.size,
// then endAngle = startAngle + 2*pi and we form a complete circle.
var endAngle = startAngle + step * data.array.length
pie = d3.pie()
.startAngle(startAngle)
.endAngle(endAngle)
.value(8) // Make each cell constant size.
}
function joinCell(data) {
var tEnter = d3.transition("cellFade")
.duration(transitionTime);
var tUpdate = d3.transition("cellChange")
.duration(transitionTime);
return svg.selectAll(".cell")
.data(pie(data.array))
.join(
enter => enter.append("path")
.attr("class", "cell")
.attr("id", (d,i) => "cell_"+i) //Unique id for each slice
.attr("d", arc) // Give it an arc
.attr("fill", "white")
.call(enter => enter.transition(tEnter)
.attr("fill", (d) => colorScale(d.data))),
update => update
.attr("d", arc)
.call(update => update.transition(tUpdate)
.attr("fill", (d) => colorScale(d.data))),
exit => exit.remove()
);
}

// Code taken from https://www.visualcinnamon.com/2015/09/placing-text-on-arcs.html
// All credit to him, none to me!
function invertArcIfBottom(data){
//A regular expression that captures all in between the start of a string
//(denoted by ^) and the first capital letter L
var firstArcSection = /(^.+?)L/;
//The [1] gives back the expression between the () (thus not the L as well)
//which is exactly the arc statement
var newArc = firstArcSection.exec(arc(data))[1];
//Replace all the comma's so that IE can handle it -_-
newArc = newArc.replace(/,/g , " ");
//If the end angle lies beyond a quarter of a circle (90 degrees or pi/2)
//flip the end and start position
if (data.endAngle > 90 * Math.PI/180) {
//Everything between the capital M and first capital A
var startLoc = /M(.*?)A/;
//Everything between the capital A and 0 0 1
var middleLoc = /A(.*?)0 0 1/;
//Everything between the 0 0 1 and the end of the string (denoted by $)
var endLoc = /0 0 1 (.*?)$/;
//Flip the direction of the arc by switching the start and end point
//and using a 0 (instead of 1) sweep flag
var newStart = endLoc.exec( newArc )[1];
var newEnd = startLoc.exec( newArc )[1];
var middleSec = middleLoc.exec( newArc )[1];

//Build up the new arc notation, set the sweep-flag to 0
newArc = "M" + newStart + "A" + middleSec + "0 0 0 " + newEnd;
}
return newArc;
}

function joinCellTextArc(data){
return svg.selectAll(".cellHidden")
.data(pie(data.array))
.join(
enter => enter
.append("path")
.attr("class", "cellHidden")
.attr("id", (d,i) => "donutArc"+i)
.attr("d", d => invertArcIfBottom(d))
.attr("fill", "none"),
update => update
.attr("d", d => invertArcIfBottom(d)),
exit => exit.remove()
)
}

function joinCellText(data) {
var tEnter = d3.transition("textEnter")
.duration(transitionTime);
var tUpdate = d3.transition("textUpdate")
.duration(transitionTime);
return svg.selectAll(".cellText")
.data(pie(data.array))
.join(
enter => enter.append("text")
.attr("class", "cellText")
// Move the labels below the arcs for slices with an end angle > than 90 degrees
.attr("dy", (d,i) => (d.endAngle > 90 * Math.PI/180 ? -1 : 18) )
.append("textPath")
.attr("startOffset","50%")
.attr("xlink:href", (d,i) => "#donutArc"+i )
// Create a _current property to memoize last value, for the later update()
.property("_current", d => d.data)
.text(d => toHex(d.data))
.call(enter => enter.transition(tEnter)
// Transition: fade to black
.attrTween("fill", _ => d3.interpolate("white", "black"))),
update => update
.select("textPath")
.call(update => update.transition(tUpdate)
.textTween(
function(d) {
const i = interpol(this._current, d.data)
return function(t) {
return toHex(this._current = Math.round(i(t)));
};
})),
exit => exit.remove()
);
}

function joinHead(data) {
var t = d3.transition("head")
.duration(2 * transitionTime / 3);
// Make sure to have the same behavior as actual head(),
// but just return the index instead of the actual value
var actualHead = mod(data.head - 1, data.size)
return svg.selectAll("#head")
.data(
[ pie(data.array)[actualHead] ] // Data is an array of one element
)
.join(
enter => enter
.append("circle")
.attr("id", "head")
.attr("cx", d => arc.centroid(d)[0])
.attr("cy", d => arc.centroid(d)[1])
.attr("r", 10),
update => update
.call(update => update
// End the other transition, see
// https://bl.ocks.org/Andrew-Reid/d92de15ef9694f12cf5695271dd73cb8
.finish() // No, this isn't included in d3 by default!
.transition(t)
.attr("cx", d => arc.centroid(d)[0])
.attr("cy", d => arc.centroid(d)[1]))
// Make sure the head element is correctly ordered at the top
.raise(),
exit => exit.remove()
);
}
function joinTail(data) {
var t = d3.transition("tail")
.duration(transitionTime / 2);
var actualTail = data.tail
return svg.selectAll("#tail")
.data(
[ pie(data.array)[actualTail] ] // Data is an array of one element
)
.join(
enter => enter
.append("rect")
.attr("id", "tail")
.attr("x", d => arc.centroid(d)[0] - 15)
.attr("y", d => arc.centroid(d)[1] - 15)
.attr("width", 30)
.attr("height", 30),
update => update
.call(update => update
.finish()
.transition(t)
.attr("x", d => arc.centroid(d)[0] - 15)
.attr("y", d => arc.centroid(d)[1] - 15))
.raise(),
exit => exit.remove()
);
}

/* To update:
* - Change the start and end angle of the pie. We represent a circular buffer with
* fixed size, so if the buffer is empty, then it needs to start at the according
* angle.
* - Join each of our chart elements. See https://bost.ocks.org/mike/selection/
* for the idea,
* and https://observablehq.com/@d3/selection-join for the pattern used here.
*/
function update(data) {
updateStartEndAngles(data) // Mutates pie!
joinCell(data)
joinCellTextArc(data)
joinCellText(data)
joinHead(data)
joinTail(data)
}
update(data) // Run the initial chart.
})
};
my.transitionTime = function(value) {
if (!arguments.length) return transitionTime;
transitionTime = value;
return my;
};
return my;
}
Insert cell
{
var normal_buf = new CircularBuffer(8, [0x0, 0x200, 0x300]);
const normal_sel = d3.create("div")
.attr("class", "buffer")
.datum(normal_buf)
var normalBuffer = chart(normal_sel)
normal_sel.append("button")
.text("Add RNG value")
.on("click", function() {
normal_buf.consume()
normalBuffer() // Render again
});
normalBuffer()

return normal_sel.node()
}
Insert cell
html`<style>
.buffer button {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
margin-bottom: 1rem;
}
.buffer svg {
max-width: 100%;
height: auto;
display: block;
margin: 0 auto;
}
.buffer circle {
fill: none;
stroke: black;
stroke-width: 2px;
}
.buffer rect {
fill: none;
stroke: black;
stroke-width: 2px;
}
.cellText textPath {
text-anchor: middle;
font-family: Fira Code;
}
.cell {
stroke: black;
stroke-width: 0.3px;
}
.results {
font-family: Fira Code;
}
@media (max-width: 600px) {
.cellText textPath {
font-size: 12px;
}
}
</style>`
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