data = d3.csvParse(await FileAttachment("calculation/recommerce.csv").text(), d3.autoType)
chart = {
//| replay;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);
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]);
updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);
invalidation.then(() => svg.interrupt());
await transition.end();
}
}
duration = 500
n = 10
names = new Set(data.map(d => d.name))
datevalues = Array.from(d3.rollup(data, ([d]) => d.value, d => +d.date, d => d.name))
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b))
function rank(value) {
const data = Array.from(names, name => ({name, value: value(name)}));
data.sort((a, b) => d3.descending(a.value, b.value));
for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
return data;
}
k = 20
keyframes = {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
new Date(ka * (1 - t) + kb * t),
rank(name => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t)
]);
}
}
keyframes.push([new Date(kb), rank(name => b.get(name) || 0)]);
return keyframes;
}
nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name)
prev = new Map(nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a])))
next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)))
function bars(svg) {
let bar = svg.append("g")
.attr("fill-opacity", 0.05)
.selectAll("rect");
return ([date, data], transition) => bar = bar
.data(data.slice(0, n), d => d.name)
.join(
enter => enter.append("rect")
.attr("fill", color)
.attr("height", y.bandwidth())
.attr("x", x(0))
.attr("y", d => y((prev.get(d) || d).rank))
.attr("width", d => x((prev.get(d) || d).value) - x(0)),
update => update,
exit => exit.transition(transition).remove()
.attr("y", d => y((next.get(d) || d).rank))
.attr("width", d => x((next.get(d) || d).value) - x(0))
)
.call(bar => bar.transition(transition)
.attr("y", d => y(d.rank))
.attr("width", d => x(d.value) - x(0)));
}
function labels(svg) {
let label = svg.append("g")
.attr("text-anchor", "end")
.attr("fill-opacity", 0.15)
.attr("font-size", "8")
.selectAll("text");
return ([date, data], transition) => label = label
.data(data.slice(0, n), d => d.name)
.join(
enter => enter.append("text")
.attr("transform", d => `translate(${x((prev.get(d) || d).value)},${y((prev.get(d) || d).rank)})`)
.attr("y", y.bandwidth() / 2)
.attr("x", -6)
.attr("dy", ".3em")
.text(d => d.name)
.call(text => text.append("tspan")
.attr("fill-opacity", 0.0) //ACHTUNG
.attr("font-weight", "normal")
.attr("font-size", "10")
.attr("x", -6)
.attr("dy", "0.9em")),
update => update,
exit => exit.transition(transition).remove()
.attr("transform", d => `translate(${x((next.get(d) || d).value)},${y((next.get(d) || d).rank)})`)
.call(g => g.select("tspan")
.textTween((d) => d3.interpolateRound(d.value, (next.get(d) || d).value))
)
)
.call(bar => bar.transition(transition)
.attr("transform", d => `translate(${x(d.value)},${y(d.rank)})`)
.call(g => g.select("tspan")
.textTween((d) => (t) => formatNumber(
d3.interpolateNumber((prev.get(d) || d).value, d.value)(t)
))
)
)
}
formatNumber = d3.format(",d")
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.selectAll(".tick text").style("opacity", 0.0); // Setting opacity for tick labels
g.select(".domain").remove();
};
}
function ticker(svg) {
const part1 = "Proud of "; // First part of the string
const coloredPart = "extending product lifecycles"; // The part you want in blue
const part2 = " in "; // Last part of the string
const now = svg.append("text")
.attr("text-anchor", "end")
.attr("x", width/ 1.6)
.attr("y", margin.top + barSize * (n + 0.45))
.attr("dy", "0.32em")
.style("opacity", 0.95) // Setting opacity for the year label
.attr("font-weight", "normal")
.attr("font-size", "9");
// Append first part of the string
now.append("tspan")
.text(part1);
// Append the colored part
now.append("tspan")
.text(coloredPart)
.style("fill", "#2a9df4"); // Coloring the text in blue
// Append the last part of the string
now.append("tspan")
.text(part2);
// Add the initial date to the end
now.append("tspan")
.text(formatDate(keyframes[0][0]));
return ([date], transition) => {
// Update the text during transitions
transition.end().then(() => {
now.selectAll("tspan").remove(); // Remove all tspans to avoid duplication
// Re-append all parts with the updated date
now.append("tspan")
.style("opacity", 0.7)
.text(part1);
now.append("tspan")
.text(coloredPart)
.style("fill", "#2a9df4"); // Coloring the text in blue// Ensure the colored part remains blue
now.append("tspan")
.style("opacity", 0.7)
.text(part2);
now.append("tspan")
.style("opacity", 0.7)
.text(formatDate(date));
});
};
}
formatDate = d3.utcFormat("%Y")
color = {
const scale = d3.scaleOrdinal(d3.schemeTableau10);
if (data.some(d => d.category !== undefined)) {
const categoryByName = new Map(data.map(d => [d.name, d.category]))
scale.domain(Array.from(categoryByName.values()));
return d => scale(categoryByName.get(d.name));
}
return d => scale(d.name);
}
x = d3.scaleLinear([0, 1], [margin.left, width - margin.right])
y = d3.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1)
height = margin.top + barSize * n + margin.bottom
barSize = 16
margin = ({top: 16, right: 6, bottom: 20, left: 0})
d3 = require("d3@7")
Moin, I’m Moritz.
I am a digital business management professional and certified Scrum Master.
As the Head of Sales at a recommerce company, I promote the transition to a circular economy on a daily basis. Together with a dedicated team, I refine our sustainable business model and optimize our positioning within digital business ecosystems.
Currently, I am particularly interested in the optimization of pricing in recommerce markets based on artificial neural networks.
You can reach me via social media or contact me directly by email.
The word “moin” is an informal regional greeting commonly used in northern Germany. It is used at any time of the day and can be translated to “hi” or “hello”.