Spacing text mark on a bar when stacked bars are close

I am using Plot.barX() with stacked data, which ends up looking like:

There are 3 values in the block, and I am using a Plot.text() to display the value above the bar, but when the 3rd value is very small, the text marks end up on top of each other.

I am already using the dx option to offset them slightly but dx can’t vary by value (as far as I can tell) so I can’t dynamically offset each text mark based on its value (or other values), it only accepts an integer, not a function.

I would love to be able to calculate/resolve some better spacing around my text marks.

I thought about using a different data set (derived from the main stackedTypes) to use with the Text mark that would force different offsets for those marks, but this feels convoluted.

Any thoughts on a better idea? Or is that my best option?

Here’s my whole (relevant) code:

Plot.plot({
    title: "Content Types Used / Created / Available",
    width,
    x: { label: null },
    y: { label: null },
    height: 100,
    marks: [
      Plot.barX(stackedTypes, {
        x: "barValue",
        tick: null
      }),
      Plot.axisY({ ticks: [] }),
      Plot.text(stackedTypes, {
        x: "value",
        y: "name",
        dx: -10,
        dy: -35,
        textAnchor: "end"
      }),
      Plot.text(stackedTypes, {
        x: "value",
        y: "name",
        text: (d) => (d.barValue == 0) ? '' : d.label,
        dx: -10,
        dy: -50,
        textAnchor: "end"
      }),
      Plot.ruleX([0])
    ],
  })

You could do three text marks, to anchor them to start, middle, then end.

If you’re looking for a programmatic solution, maybe the “occlude” transform that we’re experimenting with (at occlusionY initializer by Fil · Pull Request #1957 · observablehq/plot · GitHub) could help.

Thank you Fil,

I did try separate text marks using the select transform, but it didn’t have the result I wanted.

In the end I duplicated the bar dataset for the text marks and wrote some code to adjust the values to account for the text padding when the x values were too close to each other on both ends. Since I know there’s only ever a max of 3 values in that bar, and the 3 labels don’t change, it was easy enough to do it this way, and I wrapped it all in a component.

I did also try the dodgeX transform but that didn’t work to keep the text marks aligned with the bar, so I don’t think Dodge does what I think it does.

I will keep an eye on your spread transform for the future, that does sound more like what I would have needed, thank you!

1 Like

Can you share a visual of the solution?

Sure…

Here’s a few examples where the labels are adjusted.

For x[0] and x[1] being too close on the left, I offset x[1] to the right:

For x[1] and x[2] being too close, I offset x[2] to the left (and potentially also x[0] if it is also too close to x[1]):

And even for all 3 being too close to each other:

But when there’s no crowding issues, the values don’t get offset:

I am basically calculating offsets for x[0] and x[1] with some slightly fudgy math using the dynamic width passed into the component (from the resize helper), the value of x[2] (which represents the scale of the bar) and constants that represent the length of the labels.

The offsets look something like this:

const createdOffset = (39 / width) * (totalTypes);
const usedOffset = (55 / width) * (totalTypes);

39 and 55 are the constants that I figured out through trial and error to represent the lengths of the labels that need to be accounted for.

totalTypes here is x[2] (which is the sum of “Created” and “Avail” in the bars above).

Then I have some logic that compares the various x values to determine whether they are close enough to each other that they need the offset applied, and then applies them to the copy of the data set that is used in the text marks.

I’m not super happy with the solution, it’s a lot of linear code and if/else statements. I may try to think about a better way to do this later, but for now it works.

1 Like