Plot Label and Tick Issues

I’ve got two issues so I’ll describe them separately.

Issue 1:

I’m working on a graph where I need to show fewer dates on the x-axis but not move around any data points. I also need to push the date label down a tiny bit. If I try and use ticks: 10, it changes nothing.

Here’s an example
Screenshot 2024-07-11 at 11.33.07 AM

Code for the x-axis

    x: {
      label: "Date",
      tickFormat: (d, i, ticks) => {
        const date = new Date(d);
        const day = date.getDate();
        const month = date.toLocaleDateString("en-US", { month: "short" });

        if (i === 0 || day < prevNum) {
          prevNum = day;
          return `${month} ${day}`;
        }
        prevNum = day;
        return `${day}`;
      },
    },

Issue 2:

I need to add padding to the inside of the Plot because my y-axis ticks are cut off (I set style: {fontSize: "14px" }).

The 1 in 1,000 is cut off on the left side of the plot so it looks like ,000 instead of 1,000.

Code for the y-axis:

   y: {
      grid: true,
      label: "Value",
      domain: yDomain,
    },

Plot Options Code:

const plotOptions = {
    x: {
      label: "Date",
      tickFormat: (d, i, ticks) => {
        const date = new Date(d);
        const day = date.getDate();
        const month = date.toLocaleDateString("en-US", { month: "short" });

        if (i === 0 || day < prevNum) {
          prevNum = day;
          return `${month} ${day}`;
        }
        prevNum = day;
        return `${day}`;
      },
    },
    y: {
      grid: true,
      label: "Value",
      domain: yDomain,
    },
    insetTop: 5,
    insetBottom: 5,
    height: 400,
    width: 1700,
    style: {
      fontSize: "14px",
    },
    marks: [
      Plot.lineY(plotData, {
        x: "date",
        y: "value",
        stroke: strokeColor(currentType),
        tip: true,
        marker: true,
        title: (d) => d.title,
      }),
    ],
}
1 Like

For Issue 1, I think you should use Javascript Date objects, rather than strings. That way, the x-scale will be treated numerically, rather than categorically.

For Issue 2, you might try the margin option.

Here’s an example that allows you to play with these things:

3 Likes

That’s a super helpful demo!

Thanks!

I’m curious why Observable Plot would need me to manually adjust the margin until it fits on the screen.

Is there no way to have the library make sure that it fits automatically?

Thanks!

Two reasons.

First, philosophically, having the margins computed automatically based on data can give unpredictable or inconsistent results. Say you have several time series and you want to compare them with small multiple charts with a shared x-axis (how did these signals vary over time?); if the default margins depend on the data because the data affects the ticks, then you could end up with a slightly different x-axis on each chart because the margins varied, making it harder to compare. Or say you’re using Plot to visualize a dynamic dataset that changes sixty times a second; if the margins depend on data, the scales could change every time the data updates. Whereas with Plot’s design, the margins are strongly predictable because they don’t depend on the values. We recommend that authors adjust the margins to some reasonable value so they are consistent (and maybe set the lineWidth axis option to control how long ticks are displayed).

Second, pragmatically, it’s hard and slow to compute text metrics accurately. When rendering SVG, the SVG must be inserted into the DOM because its appearance is affected by stylesheets. But for convenience Plot is designed to render detached SVG elements which you then insert wherever you want them. (In fact, even if Plot were to compute text metrics synchronously on rendering, it wouldn’t be correct if you had dynamic styles — to really be “correct”, Plot would have to continuously observe changes to styles and re-rendering the chart, which is an implementation nightmare!) So when Plot does need to compute text metrics, as with the lineWidth option on the text mark, we use approximate metrics that are much faster to compute even though they aren’t guaranteed to match the actual text metrics.

I could maybe see Plot doing something smarter here in the future. But the fact that it’s both hard to do (especially if you want rendering to be fast) and not always desirable means we haven’t been especially motivated to work on it. In practice, where I feel the pain most often here is large y values (say tens of thousands, or millions) — exactly the case described above; these ticks tend to get cut off by default because they overflow the bounds of the SVG with the default margins. But I find the best solution for this is not to increase the margins, but to set the transform option on the y scale to change the units (say to thousands or millions), which eliminates the need for larger margins and is more readable to boot. Say like this:

Plot.plot({
  y: {
    label: "Value (thousands)",
    transform: (y) => y / 1000
  },
  marks: [
    Plot.ruleY([0]),
    Plot.line({length: 365}, {x: d3.utcDays(new Date("2021-01-01"), new Date("2022-01-01")), y: (_, i) => i ** 2})
  ]
})

I want Plot to automatically detect a suitable power-of-ten unit for large values #1816 even more than I want automatic margins based on text metrics.

2 Likes

Also see #1722 for some related work that might land someday… and please upvote #383 and #1451 if you are interested in this work.

1 Like

Wow, what solid, helpful answers.

Automatically transform to thousands etc. for shorter tick labels. · Issue #395 · observablehq/plot · GitHub looks great too.

Thanks, @mbostock ! :heart: