Skip to content

D3 Bar Chart Negative Values not Showing Up

In my bar chart, I have both negative values and positive values, but the problem is, The negative bars don’t get plotted in the opposite direction (Inverted) rather, they are plotted in the same direction as the positive bars. I understand, the Y-Axis domain, can’t start with 0 when dealing with negative values. But when I use d3.min to get the minimum value and use that instead of 0 in the Y-Axis domain. The negative value bars don’t show up at all. Can anyone help me with this?

Here is how I tried to do it:

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

Whole Code

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");
});

Answer

When you create a rect you specify the top left corner with the attributes x and y and then the dimensions via height and width. Thus, you can set the attributes like this

.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

If d.value can also be negative this does not work anymore and you need to introduce the reference y(0) replacing HEIGHT and handle negative values differently. For positive values the top left corner is at the data point and the rect stretches down to the x-axis; for negative values the top left corner is at the x-axis and the rect stretches down to the data point.

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

This is equivalent to

.attr("y", d => Math.min(y(d.value), y(0)))
.attr("height", d => Math.sign(d.value) * (y(0) - y(d.value)))

Concerning the domain, you can simplify this to

y.domain([
  d3.min(data, d => Math.min(d.profit, d.revenue)),
  d3.max(data, d => Math.max(d.profit, d.revenue))
]);

using pointwise a minimum and maximum.