D3 条形图负值未显示
D3 Bar Chart Negative Values not Showing Up
在我的条形图中,我有负值和正值,但问题是,负条没有绘制在相反的方向(倒置),而是绘制在与积极的酒吧。我知道,Y 轴域在处理负值时不能以 0
开头。但是当我使用 d3.min 获取最小值并在 Y 轴域中使用它而不是 0
时。负值条根本不显示。谁能帮我解决这个问题?
我是这样尝试的:
var y0 = d3.max(data, (d) => d.profit);
var y1 = d3.max(data, (d) => d.revenue);
var y2 = d3.min(data, (d) => d.profit);
var y3 = d3.min(data, (d) => d.revenue);
var maxdomain = y1;
var mindomain = y3;
if (y0 > y1) maxdomain = y0;
if (y2 < y3) mindomain = y2;
x.domain(data.map((d) => d.month));
y.domain([mindomain, maxdomain]);
完整代码
const MARGIN = {
LEFT: 60,
RIGHT: 60,
TOP: 60,
BOTTOM: 60
};
// total width incl margin
const VIEWPORT_WIDTH = 1140;
// total height incl margin
const VIEWPORT_HEIGHT = 400;
const WIDTH = VIEWPORT_WIDTH - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = VIEWPORT_HEIGHT - MARGIN.TOP - MARGIN.BOTTOM;
const svg = d3
.select(".chart-container")
.append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM);
const g = svg.append("g");
g.append("text")
.attr("class", "x axis-label")
.attr("x", WIDTH / 2)
.attr("y", HEIGHT + 70)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("Month");
g.append("text")
.attr("class", "y axis-label")
.attr("x", -(HEIGHT / 2))
.attr("y", -60)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.text("");
const zoom = d3.zoom().scaleExtent([0.5, 10]).on("zoom", zoomed);
svg.call(zoom);
function zoomed(event) {
x.range(
[MARGIN.LEFT, VIEWPORT_WIDTH - MARGIN.RIGHT].map((d) =>
event.transform.applyX(d)
)
);
barsGroup
.selectAll("rect.profit")
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth());
barsGroup
.selectAll("rect.revenue")
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth());
xAxisGroup.call(xAxisCall);
}
const x = d3
.scaleBand()
.range([MARGIN.LEFT, VIEWPORT_WIDTH - MARGIN.RIGHT])
.paddingInner(0.3)
.paddingOuter(0.2);
const y = d3.scaleLinear().range([HEIGHT, MARGIN.TOP]);
const xAxisGroup = g
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${HEIGHT})`);
const yAxisGroup = g
.append("g")
.attr("class", "y axis")
.attr("transform", `translate(${MARGIN.LEFT},0)`);
const xAxisCall = d3.axisBottom(x);
const yAxisCall = d3
.axisLeft(y)
.ticks(3)
.tickFormat((d) => "$" + d);
const defs = svg.append("defs");
const barsClipPath = defs
.append("clipPath")
.attr("id", "bars-clip-path")
.append("rect")
.attr("x", MARGIN.LEFT)
.attr("y", 0)
.attr("width", WIDTH)
.attr("height", 400);
const barsGroup = g.append("g");
const zoomGroup = barsGroup.append("g");
barsGroup.attr("class", "bars");
zoomGroup.attr("class", "zoom");
barsGroup.attr("clip-path", "url(#bars-clip-path)");
xAxisGroup.attr("clip-path", "url(#bars-clip-path)");
d3.csv("data.csv").then((data) => {
data.forEach((d) => {
d.profit = Number(d.profit);
d.revenue = Number(d.revenue);
d.month = d.month;
});
var y0 = d3.max(data, (d) => d.profit);
var y1 = d3.max(data, (d) => d.revenue);
var maxdomain = y1;
if (y0 > y1) maxdomain = y0;
x.domain(data.map((d) => d.month));
y.domain([0, maxdomain]);
xAxisGroup
.call(xAxisCall)
.selectAll("text")
.attr("y", "10")
.attr("x", "-5")
.attr("text-anchor", "end")
.attr("transform", "rotate(-40)");
yAxisGroup.call(yAxisCall);
const rects = zoomGroup.selectAll("rect").data(data);
rects.exit().remove();
rects
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit));
rects
.enter()
.append("rect")
.attr("class", "profit")
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit))
.attr("fill", "grey");
const rects_revenue = zoomGroup.selectAll("rect.revenue").data(data);
rects_revenue.exit().remove();
rects_revenue
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue));
rects_revenue
.enter()
.append("rect")
.attr("class", "revenue")
.style("fill", "red")
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue))
.attr("fill", "grey");
});
当您创建 rect
时,您使用属性 x
和 y
指定左上角,然后通过 height
和 width
指定尺寸.因此,您可以像这样设置属性
.attr("y", d => y(d.value)) // top left corner at the data point
.attr("height", d => HEIGHT - y(d.value)) // stretch rect down to x-axis
如果 d.value
也可以是负数,这将不再有效,您需要引入引用 y(0)
替换 HEIGHT
并以不同方式处理负值。对于正值,左上角位于数据点,矩形向下延伸至 x-axis;对于负值,左上角位于 x-axis 并且矩形向下延伸到数据点。
.attr("y", d => d.value > 0 ? y(d.value) : y(0))
.attr("height", d => d.value > 0 ? y(0) - y(d.value) : y(d.value) - y(0))
这相当于
.attr("y", d => Math.min(y(d.value), y(0)))
.attr("height", d => Math.sign(d.value) * (y(0) - y(d.value)))
关于域,可以简化为
y.domain([
d3.min(data, d => Math.min(d.profit, d.revenue)),
d3.max(data, d => Math.max(d.profit, d.revenue))
]);
逐点使用最小值和最大值。
在我的条形图中,我有负值和正值,但问题是,负条没有绘制在相反的方向(倒置),而是绘制在与积极的酒吧。我知道,Y 轴域在处理负值时不能以 0
开头。但是当我使用 d3.min 获取最小值并在 Y 轴域中使用它而不是 0
时。负值条根本不显示。谁能帮我解决这个问题?
我是这样尝试的:
var y0 = d3.max(data, (d) => d.profit);
var y1 = d3.max(data, (d) => d.revenue);
var y2 = d3.min(data, (d) => d.profit);
var y3 = d3.min(data, (d) => d.revenue);
var maxdomain = y1;
var mindomain = y3;
if (y0 > y1) maxdomain = y0;
if (y2 < y3) mindomain = y2;
x.domain(data.map((d) => d.month));
y.domain([mindomain, maxdomain]);
完整代码
const MARGIN = {
LEFT: 60,
RIGHT: 60,
TOP: 60,
BOTTOM: 60
};
// total width incl margin
const VIEWPORT_WIDTH = 1140;
// total height incl margin
const VIEWPORT_HEIGHT = 400;
const WIDTH = VIEWPORT_WIDTH - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = VIEWPORT_HEIGHT - MARGIN.TOP - MARGIN.BOTTOM;
const svg = d3
.select(".chart-container")
.append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM);
const g = svg.append("g");
g.append("text")
.attr("class", "x axis-label")
.attr("x", WIDTH / 2)
.attr("y", HEIGHT + 70)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.text("Month");
g.append("text")
.attr("class", "y axis-label")
.attr("x", -(HEIGHT / 2))
.attr("y", -60)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.text("");
const zoom = d3.zoom().scaleExtent([0.5, 10]).on("zoom", zoomed);
svg.call(zoom);
function zoomed(event) {
x.range(
[MARGIN.LEFT, VIEWPORT_WIDTH - MARGIN.RIGHT].map((d) =>
event.transform.applyX(d)
)
);
barsGroup
.selectAll("rect.profit")
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth());
barsGroup
.selectAll("rect.revenue")
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth());
xAxisGroup.call(xAxisCall);
}
const x = d3
.scaleBand()
.range([MARGIN.LEFT, VIEWPORT_WIDTH - MARGIN.RIGHT])
.paddingInner(0.3)
.paddingOuter(0.2);
const y = d3.scaleLinear().range([HEIGHT, MARGIN.TOP]);
const xAxisGroup = g
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${HEIGHT})`);
const yAxisGroup = g
.append("g")
.attr("class", "y axis")
.attr("transform", `translate(${MARGIN.LEFT},0)`);
const xAxisCall = d3.axisBottom(x);
const yAxisCall = d3
.axisLeft(y)
.ticks(3)
.tickFormat((d) => "$" + d);
const defs = svg.append("defs");
const barsClipPath = defs
.append("clipPath")
.attr("id", "bars-clip-path")
.append("rect")
.attr("x", MARGIN.LEFT)
.attr("y", 0)
.attr("width", WIDTH)
.attr("height", 400);
const barsGroup = g.append("g");
const zoomGroup = barsGroup.append("g");
barsGroup.attr("class", "bars");
zoomGroup.attr("class", "zoom");
barsGroup.attr("clip-path", "url(#bars-clip-path)");
xAxisGroup.attr("clip-path", "url(#bars-clip-path)");
d3.csv("data.csv").then((data) => {
data.forEach((d) => {
d.profit = Number(d.profit);
d.revenue = Number(d.revenue);
d.month = d.month;
});
var y0 = d3.max(data, (d) => d.profit);
var y1 = d3.max(data, (d) => d.revenue);
var maxdomain = y1;
if (y0 > y1) maxdomain = y0;
x.domain(data.map((d) => d.month));
y.domain([0, maxdomain]);
xAxisGroup
.call(xAxisCall)
.selectAll("text")
.attr("y", "10")
.attr("x", "-5")
.attr("text-anchor", "end")
.attr("transform", "rotate(-40)");
yAxisGroup.call(yAxisCall);
const rects = zoomGroup.selectAll("rect").data(data);
rects.exit().remove();
rects
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit));
rects
.enter()
.append("rect")
.attr("class", "profit")
.attr("y", (d) => y(d.profit))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.profit))
.attr("fill", "grey");
const rects_revenue = zoomGroup.selectAll("rect.revenue").data(data);
rects_revenue.exit().remove();
rects_revenue
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month))
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue));
rects_revenue
.enter()
.append("rect")
.attr("class", "revenue")
.style("fill", "red")
.attr("y", (d) => y(d.revenue))
.attr("x", (d) => x(d.month) + 0.5 * x.bandwidth())
.attr("width", 0.5 * x.bandwidth())
.attr("height", (d) => HEIGHT - y(d.revenue))
.attr("fill", "grey");
});
当您创建 rect
时,您使用属性 x
和 y
指定左上角,然后通过 height
和 width
指定尺寸.因此,您可以像这样设置属性
.attr("y", d => y(d.value)) // top left corner at the data point
.attr("height", d => HEIGHT - y(d.value)) // stretch rect down to x-axis
如果 d.value
也可以是负数,这将不再有效,您需要引入引用 y(0)
替换 HEIGHT
并以不同方式处理负值。对于正值,左上角位于数据点,矩形向下延伸至 x-axis;对于负值,左上角位于 x-axis 并且矩形向下延伸到数据点。
.attr("y", d => d.value > 0 ? y(d.value) : y(0))
.attr("height", d => d.value > 0 ? y(0) - y(d.value) : y(d.value) - y(0))
这相当于
.attr("y", d => Math.min(y(d.value), y(0)))
.attr("height", d => Math.sign(d.value) * (y(0) - y(d.value)))
关于域,可以简化为
y.domain([
d3.min(data, d => Math.min(d.profit, d.revenue)),
d3.max(data, d => Math.max(d.profit, d.revenue))
]);
逐点使用最小值和最大值。