Public
Edited
May 6, 2023
Fork of TR Ranking
Insert cell
Insert cell
Insert cell
chart = {
replay;

const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, 2*height+10]);
const chart1 = svg.append("g")
const chart2 = svg.append("g")
.attr("transform", d => `translate(0,${height+10})`)
// const chart1 = d3.create("svg")
// .attr("viewBox", [0, 0, width, height]);
const updateBoxes = boxes(chart1);
const updateLabels = labels(chart1);
/*
const updateArrows = arrows(chart1);
const updateLines = linesUpdate(chart2);
*/

// yield svg.node();
yield svg.node();

for (const keyframe of keyframes) {
const transition = svg.transition()
.duration(duration)
.ease(d3.easeLinear);

// Extract the top bar’s value.
// x.domain([0, keyframe[1][0].value]);
updateBoxes(keyframe, transition);
updateLabels(keyframe, transition);
// updateArrows(keyframe, transition);
// updateLines(keyframe, transition);

invalidation.then(() => svg.interrupt());
await transition.end();
}
}
Insert cell
function boxes(svg) {
let box = svg.append("g")
// .attr("fill-opacity", 0.1)
.attr("stroke", "#ccc")
.selectAll("rect");

return ([date, data], transition) => box = box
.data(data.slice(0, totalBoxes), d => d.name)
.join(
enter => enter.append("rect")
// .attr("fill", color)
.attr("height", y.bandwidth())
.attr("y", d => d.x)
.attr("x", d => d.y+50)
.attr("width", x.bandwidth())
.attr("fill-opacity", 0),
update => update,
exit => exit.transition(transition).remove()
.attr("y", d => d.x)
.attr("x", d => d.y + 90)
.attr("fill-opacity", 0)
)
.call(box => box.transition(transition)
.attr("y", d => d.x)
.attr("x", d => d.y)
.attr("fill-opacity", 0.25)
.attr("fill", fillColor)
)
}
Insert cell
Insert cell
Insert cell
function labels(svg) {
let label = svg.append("g")
.style("font", "bold 12px var(--sans-serif)")
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "middle")
.selectAll("text");

return ([date, data], transition) => label = label
.data(data.slice(0, totalBoxes), d => d.name)
.join(
enter => enter.append("text")
.attr("transform", d => `translate(${d.y},${d.x+50})`)
.attr("x", y.bandwidth() / 2)
.attr("y", x.bandwidth() / 2)
.attr("dy", "-0.25em")
.text(d => `${d.rank}${d.name.substring(0,13)}`)
// .text(d => `${d.name.substring(0,3)} [${d.i},${d.j}] (${d.x},${d.y})`)
//.text(d => `#${d.rank} [${d.i},${d.j}]`)
// .text(d => `E#${d.code} ${d.rank} [${d.x},${d.y}]`)
.call(text => text.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", -6)
.attr("dy", "2.15em")),
update => update,
exit => exit.transition(transition).remove()
.attr("transform", d => `translate(${d.y},${d.x + 50})`)
.style("font-color", "red")
// .call(g => g.select("tspan").tween("text", d => textTween(d.value, (next.get(d) || d).value)))
)
.call(bar => bar.transition(transition)
.attr("transform", d => `translate(${d.y},${d.x})`)
.text(d => `${d.name.substring(0,13)}`)
// .text(d => `${d.name.substring(0,3)} [${d.i},${d.j}] (${d.x},${d.y})`)
// .text(d => `#${d.rank} [${d.i},${d.j}]`)
// .text(d => `#${d.code} ${d.rank} [${d.x},${d.y}]`)
// .call(g => g.select("tspan").tween("text", d => textTween((prev.get(d) || d).value, d.value)))
)
}
Insert cell
Insert cell
rawData = d3.csvParse(await FileAttachment("cpi@1.csv").text(), d3.autoType);
Insert cell
Insert cell
nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name)
Insert cell
Insert cell
Insert cell
datevalues = Array.from(d3.rollup(data, d => d[0], d => +d.date, d => d.name))
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b))
Insert cell
dates = Array.from(new Set(data.map(d => +d.date)), ts=>new Date(ts))
Insert cell
duration = 300
Insert cell
k = 6 // interpolations
Insert cell
Insert cell
Insert cell
keyframes = {
const keyframes = [];
let ka, a, kb, b;
const frameColors = new Map(Array.from(names).map(
name => [name, name == selectedCountry ? colors.selected : colors.neutral])
)
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
let targets = Array.from(names, (name) => {
const d1 = a.get(name);
const d2 = b.get(name);
return {
name: name, current: d1.rank, from: d1.rank, initial: d1.rank, target: d2.rank, done: false };
});
const currentMap = new Map(targets.map((d) => [d.current, d]));
const targetsMap = new Map(targets.map((d) => [d.name, d]));
let targetSelected = targetsMap.get(selectedCountry);
targets = targets.filter(
(d) => d.current <= totalBoxes || d.target <= totalBoxes
);
targets.forEach((d) => {
d.current = Math.min(d.current, totalBoxes + 1);
d.target = Math.min(d.target, totalBoxes + 1);
d.diffSelected = d.current - targetSelected.current;
});

targets.sort((c1, c2) => {
const val1 = Math.min(c1.current, c1.target);
const val2 = Math.min(c2.current, c2.target);
if (val1 < val2) return -1;
else if (val2 < val1) return 1;
else {
var diff1 = Math.abs(c1.current - c1.target);
var diff2 = Math.abs(c2.current - c2.target);
if (diff1 == diff2) return c1.current - c2.current;
else return diff1 - diff2;
}
});
const toMove = new Set()
toMove.add(targets[0].name)
let moveAtOnce = 3;
while (targets.length > 0) {
const frameData = _.cloneDeep(Array.from(a.values()));
frameData.sort((d1,d2) => {
if (d1.rank == d2.rank) return d1.cum_value - d2.cumValue;
else return d1.rank - d2.rank;
})
frameData.forEach(d => {
const moveData = targetsMap.get(d.name);
if (toMove.has(d.name)) {
d.rank = moveData.current;
if (moveData.target == moveData.current) {
moveData.done = true;
toMove.delete(d.name);
}
else {
const from = moveData.from
if (toMove.size < moveAtOnce) {
const movingTo = currentMap.get(moveData.target);
toMove.add(movingTo.name);
}
let speed = Math.max(2, Math.floor(Math.abs(moveData.current - moveData.target) / 2));
if (targetSelected.done && moveData.current > targetSelected.curret) {
speed += 2;
moveAtOnce = 5;
}
if (Math.abs(moveData.diffSelected) <= 5) {
speed = 1;
}
if (moveData.target < moveData.current) {
moveData.current = Math.max(moveData.target, moveData.current - speed);
}
else {
moveData.current = Math.min(moveData.target, moveData.current + speed);
}
}
}
else if (targetsMap.has(d.name)) {
if (moveData.done) {
d.rank = moveData.target;
}
else {
d.rank = moveData.current;
}
}
d.i = (d.rank-1) % boxesPerRow;
d.j = Math.floor((d.rank-1) / boxesPerRow);
if (d.j % 2 == 1) {
d.i = boxesPerRow - d.i - 1
}
d.x = x(d.i);
// d.x += (d.j % 2 == 0) ? -7 : 7;
d.y = d.j < rowsCount ? y(d.j) : y.range()[1] + 50;
})
const datesDiff = kb - ka;
frameData.forEach(d => {
const countryTarget = targetsMap.get(d.name);
const diffBefore = countryTarget.diffSelected;
const diffAfter = countryTarget.current - targetSelected.current;
if (diffBefore <= 0 && diffAfter > 0) {
if (frameColors.get(d.name) == colors.neutral)
frameColors.set(d.name, colors.passedBy)
else
frameColors.set(d.name, colors.neutral)
}
if (diffBefore >= 0 && diffAfter < 0) {
if (frameColors.get(d.name) == colors.neutral)
frameColors.set(d.name, colors.passes)
else
frameColors.set(d.name, colors.neutral)
}
countryTarget.diffSelected = diffAfter;
d.color = frameColors.get(d.name);
if (countryTarget.done && !d.line) {
const line = {
x1: x_lines(countryTarget.initial),
y1: y_lines(new Date(ka)),
x2: x_lines(countryTarget.current),
y2: y_lines(new Date(kb)),
};
d.line = line;
}
countryTarget.from = countryTarget.current;
})
keyframes.push([new Date(ka), frameData.filter(d => d.rank <= totalBoxes)]);
targets = targets.filter(d => !d.done);
if (toMove.size < moveAtOnce && toMove.size != targets.length) {
const notMoving = targets.filter(d => !toMove.has(d.name))
if (notMoving.length > 0) {
toMove.add(notMoving[0].name);
}
}
// break;
}
}
return keyframes;
}
Insert cell
colors = ({selected: "orange", passedBy: "red", passes: "green", neutral: "lightblue"})
Insert cell
selectedCountry = "Turkey"
Insert cell
[Math.round(0.5), Math.round(-0.5)]
Insert cell
keyframes.map(d => d[1].filter(d => d.name == "Turkey")).map(arr => arr[0])
Insert cell
Insert cell
d3.group(rawData, d => d.name).get("Gambia")
Insert cell
x = d3.scaleBand()
.domain(d3.range(boxesPerRow))
.rangeRound([margin.left, width - margin.right])
.padding(0.1)
Insert cell
y = d3.scaleBand()
.domain(d3.range(rowsCount))
// .rangeRound([margin.top, margin.top + barHeight * (rowsCount + 1 + 0.1)])
.rangeRound([margin.top, margin.top + barHeight * (rowsCount + 1 + paddingY)])
.padding(paddingY)
Insert cell
x_lines = d3.scaleLinear()
.domain([0,totalBoxes])
.range([margin.left, width - margin.right])
Insert cell
x_lines.domain()
Insert cell
y_lines = d3.scaleTime()
.domain(d3.extent(dates))
.range([0,height])
Insert cell
x_lines.domain()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
_ = require("lodash@4")
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