Why won't my dates works with barY?

I’m trying to do a stacked bar chart to show cash flow. My attempt here kinda works

async function cashflow() {
  const data = await d3.csv("cashflow.csv");
  const options = {
    marginLeft: 50,
    title: "Cash Flow",
    subtitle: htl.html`<h4><span style="color:green">Operating</span> + <span style="color:gold">Investing</span> + <span style="color:red">Financing</span></h4>`,
    caption: "Black line is net cash",
    x: {
      transform: (d) => new Date(d),
      grid: true
    },
    y: {
      transform: (d) => +d,
      line: true,
      grid: true
    },
    marks: [
      Plot.barY(data, {x: "FY", y1: "Investing", y2: "Operating", fill: "green", opacity: 0.7}),
      Plot.barY(data, {x: "FY", y1: "Financing", y2: "Investing", fill: "gold", opacity: 0.7}),
      Plot.barY(data, {x: "FY", y: "Financing", fill: "red", opacity: 0.7}),
      Plot.barY(data, {x: "FY", y: (d) => (+d.Operating + +d.Investing + +d.Financing), stroke: "black"}),
      Plot.lineY(data, {x: "FY", y: (d) => (+d.Operating + +d.Investing + +d.Financing), stroke: "black"}),
      Plot.ruleY([0])
    ]
  };
  const plot = Plot.plot(options);
  const div = document.querySelector("#cashflow");
  div.append(plot);
}

cashflow();

My example data looks like

FY Operating Investing Financing
2015/03/31 159 -552 551
2016/03/31 78 -1384 1892
2017/03/31 -40 2814 -411
2018/03/31 -158 7853 -388
2019/03/31 410 -7326 -2043
2020/03/31 -196 2316 6
2021/03/31 73 -731 53
2022/03/31 -702 5906 894
2023/03/31 -177 12555 -12096

While I get a graph along the lines I want, it comes with this warning

Warning: some data associated with the x scale are dates. Dates are typically associated with a “utc” or “time” scale rather than a “band” scale. If you are using a bar mark, you probably want a rect mark with the interval option instead; if you are using a group transform, you probably want a bin transform instead. If you want to treat this data as ordinal, you can specify the interval of the x scale (e.g., d3.utcDay), or you can suppress this warning by setting the type of the x scale to “band”.

What I’ve tried is

      Plot.rectY(data, {x: "FY", y1: "Investing", y2: "Operating", fill: "green", opacity: 0.7, interval: d3.utcYear}),
      Plot.rectY(data, {x: "FY", y1: "Financing", y2: "Investing", fill: "gold", opacity: 0.7, interval: d3.utcYear}),
      Plot.rectY(data, {x: "FY", y: "Financing", fill: "red", opacity: 0.7, interval: d3.utcYear}),
      Plot.rectY(data, {x: "FY", y: (d) => (+d.Operating + +d.Investing + +d.Financing), stroke: "black"}),

which doesn’t work (no errors, just no output).

If I use areaY instead of barY, the error message about the dates goes, but I want a bar chart.

I think you need to set interval: "year" instead of d3.utcYear.

1 Like

The problem is data types. You are using d3.csv to load the data, which means that by default all the fields will be strings; you need to parse strings to produce dates and numbers. While you somewhat correct for this by using the transform option on the x and y scales, this doesn’t work in conjunction with the interval option. (d3.utcYear("2023/03/31") returns Invalid Date.)

You should instead fix the data types when you load the data. One way to do that would be using d3.autoType:

const data = await d3.csv("cashflow.csv", d3.autoType);

However, this will only work if your dates and numbers are in the standard format. If your dates are not in ISO8601 (i.e., YYYY-MM-DD), then you’ll need to use a date parser to correct for this, say:

const parseDate = d3.utcParse("%Y/%m/%d");
const data = await d3.csv("cashflow.csv", (d) => ({
  FY: parseDate(d.FY),
  Operating: +d.Operating,
  Investing: +d.Investing,
  Financing: +d.Financing,
}));

Once you do this, you can remove the transform scale options, and use the interval option that the warning suggests. (I also recommend using the named interval year rather than d3.utcYear, though they do the same thing.)

Plot.plot({
  marginLeft: 50,
  title: "Cash Flow",
  subtitle: htl.html`<h4><span style="color:green">Operating</span> + <span style="color:gold">Investing</span> + <span style="color:red">Financing</span></h4>`,
  caption: "Black line is net cash",
  grid: true,
  y: {line: true},
  marks: [
    Plot.rectY(data, {x: "FY", y1: "Investing", y2: "Operating", fill: "green", opacity: 0.7, interval: "year"}),
    Plot.rectY(data, {x: "FY", y1: "Financing", y2: "Investing", fill: "gold", opacity: 0.7, interval: "year"}),
    Plot.rectY(data, {x: "FY", y: "Financing", fill: "red", opacity: 0.7, interval: "year"}),
    Plot.rectY(data, {x: "FY", y: (d) => (+d.Operating + +d.Investing + +d.Financing), stroke: "black", interval: "year"}),
    Plot.lineY(data, {x: "FY", y: (d) => (+d.Operating + +d.Investing + +d.Financing), stroke: "black"}),
    Plot.ruleY([0])
  ]
})

Another possibility here is that you can use the interval scale option on the x scale, treating the time as ordinal rather than continuous. That looks like this:

Plot.plot({
  marginLeft: 50,
  title: "Cash Flow",
  subtitle: htl.html`<h4><span style="color:green">Operating</span> + <span style="color:gold">Investing</span> + <span style="color:red">Financing</span></h4>`,
  caption: "Black line is net cash",
  grid: true,
  x: {interval: "year"},
  y: {line: true},
  marks: [
    Plot.barY(data, {x: "FY", y1: "Investing", y2: "Operating", fill: "green", opacity: 0.7}),
    Plot.barY(data, {x: "FY", y1: "Financing", y2: "Investing", fill: "gold", opacity: 0.7}),
    Plot.barY(data, {x: "FY", y: "Financing", fill: "red", opacity: 0.7}),
    Plot.barY(data, {x: "FY", y: (d) => (+d.Operating + +d.Investing + +d.Financing), stroke: "black"}),
    Plot.lineY(data, {x: "FY", y: (d) => (+d.Operating + +d.Investing + +d.Financing), stroke: "black"}),
    Plot.ruleY([0])
  ]
})

I prefer the ordinal approach here, especially since the start of the fiscal year isn’t the same as the calendar year.

Live notebook: Plot: Why won’t my dates work with barY? / Observable | Observable

1 Like

Many thanks for the prompt help.

This my current version:

async function cashflow() {
  const parseDate = d3.utcParse("%Y/%m/%d");

  const data = await d3.csv("cashflow.csv", (d) => ({
    FY: parseDate(d.FY),
    Operating: +d.Operating,
    Investing: +d.Investing,
    Financing: +d.Financing,
  }));

  const options = {
    marginLeft: 50,
    title: "Cash Flow",
    subtitle: htl.html`<h4><span style="color:green">Operating</span> + <span style="color:gold">Investing</span> + <span style="color:red">Financing</span></h4>`,
    caption: "Blue line is net cash",
    grid: true,
    marks: [
      Plot.rectY(data, {x: "FY", interval: "year", y1:  "Investing", y2: "Operating",  fill: "green", opacity: 0.7}),
      Plot.rectY(data, {x: "FY", interval: "year", y1: "Financing", y2: "Investing",  fill: "gold", opacity: 0.7}),
      Plot.rectY(data, {x: "FY", interval: "year", y: "Financing", fill: "red", interval: "year", opacity: 0.7}),
      Plot.rectY(data, {x: "FY", interval: "year", y: (d) => (+d.Operating + +d.Investing + +d.Financing), stroke: "blue", strokeWidth: 2}),
      Plot.ruleY([0], {stroke: "black", strokeWidth: 1}),
    ]
  };
  const plot = Plot.plot(options);
  const div = document.querySelector("#cashflow");
  div.append(plot);
}

I only just picked up on that problem now, and realised a better way was to reshape the input data and use Plot’s automated barY stacking like so:

async function cashflow() {
  const parseDate = d3.utcParse("%Y/%m/%d");

  let data = await d3.csv("cashflow.csv", (d) => ([
    {FY: parseDate(d.FY), type: "Operating", value: +d.Operating, total: (+d.Operating + +d.Investing + +d.Financing)},
    {FY: parseDate(d.FY), type: "Investing", value: +d.Investing, total: 0},
    {FY: parseDate(d.FY), type: "Financing", value: +d.Financing, total: 0}
  ]));
  data = data.flat();

  const options = {
    marginLeft: 50,
    title: "Cash Flow",
    subtitle: htl.html`<h4><span style="color:lightblue">Operating</span> + <span style="color:gold">Investing</span> + <span style="color:red">Financing</span></h4>`,
    caption: "Green line is net cash",
    x: {interval: "year"},
    grid: true,
    color: {legend: true},
    marks: [
      Plot.barY(data, {x: "FY", y: "value", fill: "type", order: "appearance"}),
      Plot.barY(data, {x: "FY", y: "total", stroke: "green", strokeWidth: 3}),
      Plot.ruleY([0], {stroke: "black", strokeWidth: 1}),
    ]
  };
  const plot = Plot.plot(options);
  const div = document.querySelector("#cashflow");
  div.append(plot);
}