1 回答
TA贡献1824条经验 获得超8个赞
第一步是确定如何简化此图表。删除功能,直到保留最基本的东西。然后,构建它并逐渐添加功能,直到它类似于您想要的。
在您的情况下,那将是一个水平条形图。然后,添加一些负值和居中的零线。最后,降低条形的高度,使它们成为节点,然后添加文本。
我将尝试在这些步骤中添加类似这样的内容,没有布局和所有内容,但希望您能够看到我的逻辑。
基本垂直条形图
// Some fake data
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i + 2
}));
const width = 600,
height = 300
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Process it to find the x and y axis domains
// scaleLinear because it considers numbers
const x = d3.scaleLinear()
.domain([0, d3.max(data.map(d => d.value))]) // the possible values
.range([0, width]); // the available screen space
// scaleBand because it's just categorical data
const y = d3.scaleBand()
.domain(data.map(d => d.name)) // all possible values
.range([height, 0]) // little weird, y-axis is always backwards, because (0,0) is the top left
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
// Append a container element. This will hold the chart
.append('g')
// Move it a little to account for the axes and labels
.attr('transform', `translate(${margin.left} ${margin.right})`);
// Draw the bars
// First, assign the data to the bar objects, this will decide which to remove, update, and add
const bars = g.append('g')
.selectAll('rect')
.data(data);
// Good practice: always call remove before adding stuff
bars.exit().remove();
// Add the new bars and assign any attributes that do not depend on the data
// for example, font for texts
bars.enter()
.append('rect')
.attr('fill', 'steelblue')
// Now merge it with the existing bars
.merge(bars)
// From now on we operate on both the old and the new bars
// Bars are weird, first we position the top left corner of each bar
.attr('x', 0)
.attr('y', d => y(d.name))
// Then we determine the width and height
.attr('width', d => x(d.value))
.attr('height', y.bandwidth())
// Draw the x and y axes
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
现在我将删除所有旧评论并解释我所做的不同之处。
负水平条形图
// Now, the data can also be negative
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i - 5
}));
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// Now, we don't use 0 as a minimum, but get it from the data using d3.extent
const x = d3.scaleLinear()
.domain(d3.extent(data.map(d => d.value)))
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
const bars = g.append('g')
.selectAll('rect')
.data(data);
bars.exit().remove();
bars.enter()
.append('rect')
.merge(bars)
// All the same until here
// Now, if a bar is positive it starts at x = 0, and has positive width
// If a bar is negative it starts at x < 0 and ends at x = 0
.attr('x', d => d.value > 0 ? x(0) : x(d.value))
.attr('y', d => y(d.name))
// If the bar is positive it ends at x = v, but that means it's x(v) - x(0) wide
// If the bar is negative it ends at x = 0, but that means it's x(0) - x(v) wide
.attr('width', d => d.value > 0 ? x(d.value) - x(0) : x(0) - x(d.value))
.attr('height', y.bandwidth())
// Let's color the bar based on whether the value is positive or negative
.attr('fill', d => d.value > 0 ? 'darkgreen' : 'darkred')
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
现在,我将把条形更改为示例代码中的节点。
带节点的水平图表
const data = ['SaaS', 'Sales', 'Fruits & Veggies', 'IT'].map((v, i) => ({
name: v,
value: 3 * i - 5
}));
// We want to center each rect around the value it's supposed to have.
// That means that we need to have a node width
const nodeWidth = 60;
const width = 600,
height = 300,
margin = {
top: 20,
left: 100,
right: 40,
bottom: 40
};
// We also need to make sure there is space for all nodes, even at the edges.
// One way to get this is by just extending the domain a little.
const domain = d3.extent(data.map(d => d.value));
const x = d3.scaleLinear()
.domain([domain[0] - 1.5, domain[1] + 1.5])
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([height, 0])
.padding(0.1);
const svg = d3.select('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom);
const g = svg
.append('g')
.attr('transform', `translate(${margin.left} ${margin.right})`);
const bars = g.append('g')
.selectAll('rect')
.data(data);
bars.exit().remove();
// All the same until here
bars.enter()
.append('rect')
// width has become a constant
.attr('width', nodeWidth)
// Now, transform each node so it centers around the value it's supposed to have
.attr('transform', `translate(${-nodeWidth / 2} 0)`)
// Round the corners for aesthetics
.attr('rx', 15)
.merge(bars)
// `x` denotes the placement directly again
.attr('x', d => x(d.value))
.attr('y', d => y(d.name))
.attr('height', y.bandwidth())
.attr('fill', d => d.value > 0 ? 'darkgreen' : 'darkred');
// Now one more thing, we want to add labels to each node.
// `<rect>` can't have children, we we add them to the plot seperately
// using the same `data` as for the bars
const labels = g.append('g')
.selectAll('text')
.data(data);
labels.exit().remove();
labels.enter()
.append('text')
.attr('fill', 'white')
.attr('text-anchor', 'middle') // center-align the text
.attr('dy', 5) // place it down a little so it middles nicely in the node.
.merge(bars)
// `x` denotes the placement directly
.attr('x', d => x(d.value))
// Add half a bar's height to target the center of each node
.attr('y', d => y(d.name) + y.bandwidth() / 2)
// Actually fill in the text
.text(d => d.value);
g.append('g')
.classed('x-axis', true)
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x))
g.append('g')
.classed('y-axis', true)
.call(d3.axisLeft(y))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
我希望你能遵循这个。如果对本教程有任何不清楚的地方,请告诉我。
添加回答
举报