Published
Edited
May 2, 2022
2 forks
1 star
Insert cell
Insert cell
data = (await FileAttachment("category-brands.csv").csv()).map(d3.autoType)
Insert cell
Insert cell
Insert cell
Insert cell
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]); // x軸のドメイン最大値をトップのバーの値にする

// チャート部品の更新
updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);

invalidation.then(() => svg.interrupt()); // アニメーションの中断
await transition.end(); // トランジションの終了を待つ
}
}
Insert cell
Insert cell
Insert cell
Insert cell
duration = 250 // キーフレーム間の時間
Insert cell
Insert cell
Insert cell
Insert cell
data
Insert cell
Inputs.table(data)
Insert cell
Insert cell
d3.group(data, d => d.name)
Insert cell
Insert cell
data.filter(d => d.name === "Heineken")
Insert cell
Insert cell
n = 12 // 表示する順位
Insert cell
Insert cell
names = new Set(data.map((d) => d.name)) // ブランド名一覧
Insert cell
Insert cell
datevalues = Array.from(
d3.rollup(
data,
([d]) => d.value,
(d) => +d.date, // dateを数値型に変換
(d) => d.name
)
)
.map(([date, data]) => [new Date(date), data]) // dateを日付型に変換
.sort(([a], [b]) => d3.ascending(a, b)) // 時系列にソート
Insert cell
Insert cell
Insert cell
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); // ソートしているので、iがrankになる。ただし、rankの最大値をnにしている。
}
return data;
}
Insert cell
Insert cell
rank(name => datevalues[0][1].get(name))
Insert cell
Insert cell
k = 10
Insert cell
Insert cell
keyframes = {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
// 当年とその翌年とのペアでループする
for (let i = 0; i < k; ++i) {
// k分割する
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;
}
Insert cell
Insert cell
nameframes = d3.groups(keyframes.flatMap(([, data]) => data), d => d.name)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function bars(svg) {
let bar = svg.append("g").attr("fill-opacity", 0.6).selectAll("rect"); // fill-opacity属性は全バー共有なので親要素に指定

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)), // enter時に各バーの属性を設定する
(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)) // exit時にy,width属性をトランジションする
)
.call(
(bar) =>
bar
.transition(transition)
.attr("y", (d) => y(d.rank))
.attr("width", (d) => x(d.value) - x(0)) // y,width属性をトランジションする
));
}
Insert cell
Insert cell
Insert cell
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", "end")
.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", "-0.25em")
.text((d) => d.name)
.call((text) =>
text
.append("tspan") // 値のラベル用にtspan要素を追加
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", -6)
.attr("dy", "1.15em")
),
(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) => {
const i = d3.interpolateNumber(
d.value,
(next.get(d) || d).value
);
return function (t) {
return formatNumber(i(t));
};
})
// .tween("text", (d) =>
// textTween(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) => {
const i = d3.interpolateNumber(
(prev.get(d) || d).value,
d.value
);
return function (t) {
return formatNumber(i(t));
};
})
// .tween("text", (d) =>
// textTween((prev.get(d) || d).value, d.value)
// )
)
));
}
Insert cell
Insert cell
Insert cell
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function (t) {
this.textContent = formatNumber(i(t));
};
}
Insert cell
Insert cell
Insert cell
formatNumber = d3.format(",d") // 値のフォーマット関数
Insert cell
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
Insert cell
Insert cell
function ticker(svg) {
const now = svg
.append("text")
.style("font", `bold ${barSize}px var(--sans-serif)`)
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.attr("x", width - 6)
.attr("y", margin.top + barSize * (n - 0.45))
.attr("dy", "0.32em")
.text(formatDate(keyframes[0][0]));

return ([date], transition) => {
transition.end().then(() => now.text(formatDate(date)));
};
}
Insert cell
Insert cell
Insert cell
formatDate = d3.utcFormat("%Y") // 日付のフォーマット関数
Insert cell
Insert cell
color = {
const scale = d3.scaleOrdinal(d3.schemeTableau10); // マッピング用のスケール関数
if (data.some((d) => d.category !== undefined)) {
// category が存在したら
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); // categoryが存在しなかったら、nameで色を決める
}
Insert cell
Insert cell
Insert cell
Insert cell
x = d3.scaleLinear([0, 1], [margin.left, width - margin.right])
Insert cell
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
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
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