Published
Edited
Nov 16, 2019
2 forks
Importers
47 stars
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);

function update(data) {
const transition = svg.transition()
.duration(delay)
.ease(d3.easeLinear);

x.domain([0, data[data.length - 1][1]]);

updateAxis(data, transition);
updateBars(data, transition);
updateLabels(data, transition);
}

return Object.assign(svg.node(), {update});
}
Insert cell
chart.update(emojis)
Insert cell
emoji = name => String.fromCodePoint(...name.split("-").map(i => parseInt(i, 16)))
Insert cell
function bars(svg) {
let bar = svg.append("g")
.attr("fill", "#999")
.attr("fill-opacity", 0.6)
.selectAll("rect");

return (data, transition) => bar = bar
.data(data.slice(-n).reverse(), ([name]) => name)
.join(
enter => enter.append("rect")
.attr("height", y.bandwidth())
.attr("x", x(0))
.attr("y", y(n))
.attr("width", ([, value]) => x(value) - x(0)),
update => update,
exit => exit.transition(transition).remove()
.attr("y", y(n))
)
.call(bar => bar.transition(transition)
.attr("y", (d, i) => y(i))
.attr("width", ([, value]) => x(value) - x(0)));
}
Insert cell
function labels(svg) {
let label = svg.append("g")
.attr("font-weight", "bold")
.attr("font-size", barSize * 0.6)
.attr("font-family", "sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji")
.attr("text-anchor", "end")
.selectAll("text");

return (data, transition) => label = label
.data(data.slice(-n).reverse(), ([name]) => name)
.join(
enter => enter.append("text")
.attr("transform", ([, value]) => `translate(${x(value)},${y(n)})`)
.attr("y", y.bandwidth() / 2)
.attr("x", -6)
.call(text => text.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-size", 10)
.attr("font-weight", "normal")
.attr("dy", "0.32em")
.text(([, value]) => formatNumber(value)))
.call(text => text.append("tspan")
.attr("dx", "0.15em")
.attr("dy", "0.31em")
.text(([name]) => emoji(name))),
update => update,
exit => exit.transition(transition).remove()
.attr("transform", ([, value]) => `translate(${x(value)},${y(n)})`)
)
.call(bar => bar.transition(transition)
.attr("transform", ([, value], i) => `translate(${x(value)},${y(i)})`)
.call(g => g.select("tspan").tween("text", function([, value]) { return textTween(+this.textContent.replace(/,/g, ""), value); })));
}
Insert cell
function axis(svg) {
const g = svg.append("g")
.attr("transform", `translate(0,${margin.top})`);

const axis = d3.axisTop(x)
.ticks(width / 160)
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));

return (_, transition) => {
g.transition(transition).call(axis);
g.select(".tick:first-of-type text").remove();
g.selectAll(".tick:not(:first-of-type) line").attr("stroke", "white");
g.select(".domain").remove();
};
}
Insert cell
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function(t) {
this.textContent = formatNumber(i(t));
};
}
Insert cell
formatNumber = d3.format(",d")
Insert cell
n = 12
Insert cell
duration = 30000
Insert cell
delay = 250
Insert cell
x = d3.scaleLinear([0, 1], [margin.left, width - margin.right])
Insert cell
y = d3.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1)
Insert cell
height = margin.top + barSize * n + margin.bottom
Insert cell
barSize = 48
Insert cell
margin = ({top: 16, right: 6, bottom: 6, left: 0})
Insert cell
emojis = Generators.observe(notify => {
const bisect = d3.bisector(([, value]) => value);
const entries = [];
const counts = new Map;
const source = new EventSource("https://stream.emojitracker.com/subscribe/eps");
let head = null;
let tail = null;

function process(data) {
for (const [key, value] of data) {
if (counts.has(key)) {
const value0 = counts.get(key);
const value1 = value0 + value;
counts.set(key, value1);
if (value > 0) {
let i0 = bisect.left(entries, value0);
let i1 = bisect.left(entries, value1) - 1;
if (i0 <= i1) {
while (entries[i0][0] !== key) ++i0;
while (i0 < i1) entries[i0] = entries[i0 + 1], ++i0;
entries[i1] = [key, value1];
}
} else {
let i0 = bisect.right(entries, value0) - 1;
let i1 = bisect.right(entries, value1);
if (i0 >= i1) {
while (entries[i0][0] !== key) --i0;
if (value1) {
while (i0 > i1) entries[i0] = entries[i0 - 1], --i0;
entries[i1] = [key, value1];
} else {
counts.delete(key);
entries.splice(i0, 1);
}
}
}
} else {
counts.set(key, value);
entries.splice(bisect.right(entries, value), 0, [key, value]);
}
}
}

source.onmessage = ({data}) => {
data = Object.entries(JSON.parse(data));
process(data);
const node = {time: Date.now(), data, next: null};
if (tail === null) tail = head = node;
else tail = tail.next = node;
};

const timer = d3.interval(() => {
const expires = Date.now() - duration;
while (head !== null && head.time < expires) {
process(head.data.map(([key, value]) => [key, -value]));
head = head.next;
}
if (head === null) tail = null;
notify(entries);
}, delay);

return () => {
timer.stop();
source.close();
};
})
Insert cell
d3 = require("d3@5")
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