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 时,您使用属性 xy 指定左上角,然后通过 heightwidth 指定尺寸.因此,您可以像这样设置属性

.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))
]);

逐点使用最小值和最大值。