如何在 D3 中动态呈现水平堆积条形图
How to dynamically render horizontal stacked bar charts in D3
出于两个原因,我要说得有点冗长:
- 为了证明我已经努力尝试解决我提出的问题,因为互联网上有太多短暂的东西
- D3 很棒,因为它很复杂,很难完全理解。最近的主要版本强调了数据连接的使用,旨在简化一般更新模式。
因此,这可能有点写意,但我真的很想了解我的问题的答案,所以请耐心等待。
上下文
我想创建一个动态堆叠水平条形图,以可视化 single transferable voting 过程的不同阶段(准确地说是在苏格兰的地方选举中)。
D3 堆栈
这与 Bostock 的 Stacked Bar Chart, Horizontal 非常相似,它显示了按年龄组划分的美国各州的人口。
毫不奇怪,Bostock 使用 D3 的堆栈 生成器,我很失望地发现它在 Y 轴上将 SVG 矩形组织成组(svg g 元素)。进行以下基于 Bostock 的 Stacked Bar Chart, Horizontal 示例的实验:
const data = [
{month: "Jan", apples: 3840, bananas: 1920, cherries: 960, dates: 400},
{month: "Feb", apples: 1600, bananas: 1440, cherries: 960, dates: 400},
{month: "March", apples: 640, bananas: 960, cherries: 640, dates: 400},
{month: "Apr", apples: 3120, bananas: 1480, cherries: 640, dates: 400}
];
以上数据使用与 Bostock 示例中相同的方法进行旋转,并传递给 StackedBarChart()
,我在其中添加了一个过渡,从而导致呈现以下内容:
在上面的例子中,数据绑定不是按月份,而是按水果。
最终状态很好,但动态(转换后的)数据更改会很困难。
这是一个有些复杂的领域。我毫不掩饰地承认我不理解 Bostock 对堆栈的使用,取自他上面提到的例子:
// Compute a nested array of series where each series is [[x1, x2], [x1, x2],
// [x1, x2], …] representing the x-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique y- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([, I], z) => X[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => Y[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
嵌套数据连接
也许有人可以进一步阐明以上内容。也许我不了解堆栈(很可能;)。也许如果图表是垂直的而不是水平的,事情会更容易吗?不知道。
我决定放弃 D3 Stacks,转而使用数据连接嵌套数据,有点回归基础。
在general update pattern are helpful along with reading about the newish join method上重复阅读Bostock近十年的历史post,旨在进一步抽象通用更新模式。-*
以下代码是对 documentation nested data join example 的改编(只要确保在某处加载了 d3 库):
const svg = d3.select("body")
.append("svg");
function doD3() {
svg
.selectAll("g")
.data(generate, (d) => d[0].id.split("_")[0])
.join("g")
.attr("transform", (d, i) => "translate(0, " + i * 30 + ")")
.selectAll("text")
.data(d => d)
.join(enter => enter
.append("text").attr("fill", "green"),
update => update,
exit => exit.remove())
.attr("x", (d, i) => i * 50)
.text(d => d.value);
}
function generateRow(rowId, cols) {
let row = [];
for(let i = 0; i < cols; i++) {
row.push({"id": rowId + "_" + i, "value": highRandom()});
}
return row;
}
const lowRandom = d3.randomInt(3, 15);
const highRandom = d3.randomInt(1e2, 1e5);
function generate() {
const cols = lowRandom();
const rows = lowRandom();
let matrix = [];
for(let i = 0; i < rows; i++) {
matrix.push(generateRow(i, cols));
}
return matrix;
}
window.setInterval(doD3, 2000);
可能是原始的,但旨在演示成功的数据绑定与关键功能。 generate
和 generateRow
函数共同生成具有 id 和值的随机对象矩阵。
每两秒调用一次。所有节点(文本)都在输入时呈现。我哪里不对?
感谢阅读,新年快乐:))
-* 不幸的是,我无法 post 链接到 JSFiddle,因为支持的最新 D3 版本是版本 5,我正在(可能不必要地)使用最新版本 7。Bostock 已经开始使用一个平台允许进行名为 Observable 的实验,但我发现这很混乱。
这是一个使用水果数据集的例子。该图表是动画的,因此一次显示一个水果的条形图。我通过为每个水果的条形图设置不同的过渡来做到这一点 delay。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// set up
const margin = { top: 10, right: 10, bottom: 20, left: 40 };
const width = 300 - margin.left - margin.right;
const height = 200 - margin.top - margin.bottom;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// data
const data = [
{month: "Jan", apples: 3840, bananas: 1920, cherries: 960, dates: 400},
{month: "Feb", apples: 1600, bananas: 1440, cherries: 960, dates: 400},
{month: "March", apples: 640, bananas: 960, cherries: 640, dates: 400},
{month: "Apr", apples: 3120, bananas: 1480, cherries: 640, dates: 400}
];
const fruit = Object.keys(data[0]).filter(d => d != "month");
const months = data.map(d => d.month);
const stackedData = d3.stack()
.keys(fruit)(data);
const xMax = d3.max(stackedData[stackedData.length - 1], d => d[1]);
// scales
const x = d3.scaleLinear()
.domain([0, xMax]).nice()
.range([0, width]);
const y = d3.scaleBand()
.domain(months)
.range([0, height])
.padding(0.25);
const color = d3.scaleOrdinal()
.domain(fruit)
.range(d3.schemeTableau10);
// axes
const xAxis = d3.axisBottom(x).ticks(5, '~s');
const yAxis = d3.axisLeft(y);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
.call(g => g.select('.domain').remove());
svg.append("g")
.call(yAxis)
.call(g => g.select('.domain').remove());
// draw bars
// create one group for each fruit
const layers = svg.append('g')
.selectAll('g')
.data(stackedData)
.join('g')
.attr('fill', d => color(d.key));
// transition for bars
const duration = 1000;
const t = d3.transition()
.duration(duration)
.ease(d3.easeLinear);
layers.each(function(_, i) {
// this refers to the group for a given fruit
d3.select(this)
.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', d => x(d[0]))
.attr('y', d => y(d.data.month))
.attr('height', y.bandwidth())
.transition(t)
// i is the index of this fruit.
// this will give the bars for each fruit a different delay
// so that the fruits will be revealed one at a time.
// using .each() instead of a normal data join is needed
// so that we have access to what fruit each bar belongs to.
.delay(i * duration)
.attr('width', d => x(d[1]) - x(d[0]));
});
</script>
</body>
</html>
出于两个原因,我要说得有点冗长:
- 为了证明我已经努力尝试解决我提出的问题,因为互联网上有太多短暂的东西
- D3 很棒,因为它很复杂,很难完全理解。最近的主要版本强调了数据连接的使用,旨在简化一般更新模式。
因此,这可能有点写意,但我真的很想了解我的问题的答案,所以请耐心等待。
上下文
我想创建一个动态堆叠水平条形图,以可视化 single transferable voting 过程的不同阶段(准确地说是在苏格兰的地方选举中)。
D3 堆栈
这与 Bostock 的 Stacked Bar Chart, Horizontal 非常相似,它显示了按年龄组划分的美国各州的人口。
毫不奇怪,Bostock 使用 D3 的堆栈 生成器,我很失望地发现它在 Y 轴上将 SVG 矩形组织成组(svg g 元素)。进行以下基于 Bostock 的 Stacked Bar Chart, Horizontal 示例的实验:
const data = [
{month: "Jan", apples: 3840, bananas: 1920, cherries: 960, dates: 400},
{month: "Feb", apples: 1600, bananas: 1440, cherries: 960, dates: 400},
{month: "March", apples: 640, bananas: 960, cherries: 640, dates: 400},
{month: "Apr", apples: 3120, bananas: 1480, cherries: 640, dates: 400}
];
以上数据使用与 Bostock 示例中相同的方法进行旋转,并传递给 StackedBarChart()
,我在其中添加了一个过渡,从而导致呈现以下内容:
在上面的例子中,数据绑定不是按月份,而是按水果。
最终状态很好,但动态(转换后的)数据更改会很困难。
这是一个有些复杂的领域。我毫不掩饰地承认我不理解 Bostock 对堆栈的使用,取自他上面提到的例子:
// Compute a nested array of series where each series is [[x1, x2], [x1, x2],
// [x1, x2], …] representing the x-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique y- and z-value.
const series = d3.stack()
.keys(zDomain)
.value(([, I], z) => X[I.get(z)])
.order(order)
.offset(offset)
(d3.rollup(I, ([i]) => i, i => Y[i], i => Z[i]))
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
嵌套数据连接
也许有人可以进一步阐明以上内容。也许我不了解堆栈(很可能;)。也许如果图表是垂直的而不是水平的,事情会更容易吗?不知道。
我决定放弃 D3 Stacks,转而使用数据连接嵌套数据,有点回归基础。
在general update pattern are helpful along with reading about the newish join method上重复阅读Bostock近十年的历史post,旨在进一步抽象通用更新模式。-*
以下代码是对 documentation nested data join example 的改编(只要确保在某处加载了 d3 库):
const svg = d3.select("body")
.append("svg");
function doD3() {
svg
.selectAll("g")
.data(generate, (d) => d[0].id.split("_")[0])
.join("g")
.attr("transform", (d, i) => "translate(0, " + i * 30 + ")")
.selectAll("text")
.data(d => d)
.join(enter => enter
.append("text").attr("fill", "green"),
update => update,
exit => exit.remove())
.attr("x", (d, i) => i * 50)
.text(d => d.value);
}
function generateRow(rowId, cols) {
let row = [];
for(let i = 0; i < cols; i++) {
row.push({"id": rowId + "_" + i, "value": highRandom()});
}
return row;
}
const lowRandom = d3.randomInt(3, 15);
const highRandom = d3.randomInt(1e2, 1e5);
function generate() {
const cols = lowRandom();
const rows = lowRandom();
let matrix = [];
for(let i = 0; i < rows; i++) {
matrix.push(generateRow(i, cols));
}
return matrix;
}
window.setInterval(doD3, 2000);
可能是原始的,但旨在演示成功的数据绑定与关键功能。 generate
和 generateRow
函数共同生成具有 id 和值的随机对象矩阵。
每两秒调用一次。所有节点(文本)都在输入时呈现。我哪里不对?
感谢阅读,新年快乐:))
-* 不幸的是,我无法 post 链接到 JSFiddle,因为支持的最新 D3 版本是版本 5,我正在(可能不必要地)使用最新版本 7。Bostock 已经开始使用一个平台允许进行名为 Observable 的实验,但我发现这很混乱。
这是一个使用水果数据集的例子。该图表是动画的,因此一次显示一个水果的条形图。我通过为每个水果的条形图设置不同的过渡来做到这一点 delay。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// set up
const margin = { top: 10, right: 10, bottom: 20, left: 40 };
const width = 300 - margin.left - margin.right;
const height = 200 - margin.top - margin.bottom;
const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// data
const data = [
{month: "Jan", apples: 3840, bananas: 1920, cherries: 960, dates: 400},
{month: "Feb", apples: 1600, bananas: 1440, cherries: 960, dates: 400},
{month: "March", apples: 640, bananas: 960, cherries: 640, dates: 400},
{month: "Apr", apples: 3120, bananas: 1480, cherries: 640, dates: 400}
];
const fruit = Object.keys(data[0]).filter(d => d != "month");
const months = data.map(d => d.month);
const stackedData = d3.stack()
.keys(fruit)(data);
const xMax = d3.max(stackedData[stackedData.length - 1], d => d[1]);
// scales
const x = d3.scaleLinear()
.domain([0, xMax]).nice()
.range([0, width]);
const y = d3.scaleBand()
.domain(months)
.range([0, height])
.padding(0.25);
const color = d3.scaleOrdinal()
.domain(fruit)
.range(d3.schemeTableau10);
// axes
const xAxis = d3.axisBottom(x).ticks(5, '~s');
const yAxis = d3.axisLeft(y);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
.call(g => g.select('.domain').remove());
svg.append("g")
.call(yAxis)
.call(g => g.select('.domain').remove());
// draw bars
// create one group for each fruit
const layers = svg.append('g')
.selectAll('g')
.data(stackedData)
.join('g')
.attr('fill', d => color(d.key));
// transition for bars
const duration = 1000;
const t = d3.transition()
.duration(duration)
.ease(d3.easeLinear);
layers.each(function(_, i) {
// this refers to the group for a given fruit
d3.select(this)
.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', d => x(d[0]))
.attr('y', d => y(d.data.month))
.attr('height', y.bandwidth())
.transition(t)
// i is the index of this fruit.
// this will give the bars for each fruit a different delay
// so that the fruits will be revealed one at a time.
// using .each() instead of a normal data join is needed
// so that we have access to what fruit each bar belongs to.
.delay(i * duration)
.attr('width', d => x(d[1]) - x(d[0]));
});
</script>
</body>
</html>