Observable Plot - Stacked Bar Chart totals (sum) at top of each stacked bar

I am using ObservablePlot (not the base d3, but understanding that ObsPlot uses d3 underneath). I have a beautiful stacked bar chart (cannot show my pic here, will show generic pic below). All looks great except that I need to have totals (sum) of each stacked bar at the top of each stacked bar as in the generic image below.

Not a whole lotta documentation out there on how to do this using ObsPlot. I appreciate any assistance. I am not using Groups, as that did not provide the results we need for the data we have. The below javascript is working perfectly with the exception of the sum (or total) of each stack in each bar.

image

var plot2 = oPlot.plot({
    marginBottom: 5,
    width: 928,
    height: 500,
    x: { label: null },
    y: { tickFormat: ".0f", tickSpacing: 20, label: "PRA ($M)", labelAnchor: "center", labelOffset: 40 },
    color:
    {
        legend: "ramp",
        width: 540,
        label: null,
        range: data.Colors
    },
    marks: [
        oPlot.axisX({ ticks: [] }), //to hide xaxis ticks
        oPlot.barY(tidy, {
            x: "fundType",
            y: "total",
            insetLeft: 13,
            insetRight: 13,
            fill: "pra",
            sort: { color: null, x: "-y" }
        }),
        oPlot.textY(
            tidy,
            oPlot.stackY(
                {
                    x: "fundType",
                    z: "pra",
                    y: "total",
                    text: (d) => (d.total < 50 ? null :
                        (d.total).toLocaleString("en-US", {
                            minimumFractionDigits: 0, // Hide cents (if exact dollar).
                            maximumFractionDigits: 0 // Round to the nearest dollar.
                        })
                    ),
                }
            )
        )
    ]
});

I think you are looking for the group transform: group all the elements by x, and reduce y and text with the “sum” reducer.

I’ve seen that. I am not using grouping. Will you please take a look at my code as I have it and show me how I can implement it?

Yes, you could try and use the Plot.groupX transform on a separate mark that just shows the total label. Within your code, it might look like this:

oPlot.text(
  tidy,
  oPlot.groupX(
  {y: "sum", text: "sum"},
  {x: "fundType", y: "total", text: "total"}
))

Thank you very much Fil. That does indeed do what I would like; however I need it formatted to no decimal places and rounded up. Much like I have done in this snippet:
(d.total).toLocaleString(“en-US”, {
minimumFractionDigits: 0, // Hide cents (if exact dollar).
maximumFractionDigits: 0 // Round to the nearest dollar.
})

I have not yet found a way to apply that to “sum”. I see in other posts that you create a format function, but that is using d3.format. Will you please explain how to do that with ObsPlot, or should I make a new post with that question?

In that case instead of the built-in “sum” reducer, you can write your own as a function.

Plot.text(
  tidy,
  Plot.groupX(
  {
    y: "sum",
    text: (values) => d3.sum(values).toLocaleString(“en-US”, {
      minimumFractionDigits: 0, // Hide cents (if exact dollar).
      maximumFractionDigits: 0 // Round to the nearest dollar.
    })
  },
  {x: "fundType", y: "total", text: "total"}
))

Thank you Fil. That works nicely, now I just need space between that sum and the top of each vertical stacked bar. I have tried with a marginBottom and labelOffset to no avail. Cannot find anything in documentation. Here is what I have so far (note the marginBottom doesn’t work):

    var plot2 = oPlot.plot({
        marginBottom: 5,
        marginTop:20,
        width: 928,
        height: 500,
        x: { label: null },
        y: {
            tickFormat: ".0f",
            tickSpacing: 20,
            label: "PRA ($M)",
            labelAnchor: "center",
            labelOffset: 40
        },
        color:
        {
            legend: "ramp",
            width: 540,
            label: null,
            range: data.Colors
        },
        marks: [
            oPlot.axisX({ ticks: [] }), //to hide xaxis ticks
            oPlot.barY(tidy, {
                x: "fundType",
                y: "total",
                insetLeft: 13,
                insetRight: 13,
                fill: "pra",
                sort: { color: null, x: "-y" }
            }),
            oPlot.text( //this section is what gets us our totals at the top of each vertical stacked bar
                tidy,
                oPlot.groupX(
                {
                    y: "sum",
                    text: (funds) => d3.sum(funds).toLocaleString("en-US", {
                        minimumFractionDigits: 0, // Hide cents (if exact dollar).
                        maximumFractionDigits: 0, // Round to the nearest dollar.
                        marginBottom: 20
                    }),
                },
                { x: "fundType", y: "total", text: "total" }
            )),
            oPlot.textY( //this section gets us our individual bars (by PRA) in each vertical stacked bar
                tidy,
                oPlot.stackY(
                    {
                        x: "fundType",
                        z: "pra",
                        y: "total",
                        text: (d) => (d.total < 50 ? null :
                            (d.total).toLocaleString("en-US", {
                                minimumFractionDigits: 0,
                                maximumFractionDigits: 0
                            })
                        ),
                    }
                )
            )
        ]
    });

And please see attached screenshot for how it looks.

Try to add dy: -10 in the text mark options? dx and dy are available for all marks.

This is great! Thank you!

    var plot2 = oPlot.plot({
        marginBottom: 5,
        marginTop:20,
        width: 928,
        height: 500,
        x: { label: null },
        y: {
            tickFormat: ".0f",
            tickSpacing: 20,
            label: "PRA ($M)",
            labelAnchor: "center",
            labelOffset: 40
        },
        color:
        {
            legend: "ramp",
            width: 540,
            label: null,
            range: data.Colors
        },
        marks: [
            oPlot.axisX({ ticks: [] }), //to hide xaxis ticks
            oPlot.barY(tidy, {
                x: "fundType",
                y: "total",
                insetLeft: 13,
                insetRight: 13,
                fill: "pra",
                sort: { color: null, x: "-y" }
            }),
            oPlot.text( //this section is what gets us our totals at the top of each vertical stacked bar
                tidy,
                oPlot.groupX(
                {
                    y: "sum",
                    text: (funds) => d3.sum(funds).toLocaleString("en-US", {
                        minimumFractionDigits: 0, // Hide cents (if exact dollar).
                        maximumFractionDigits: 0, // Round to the nearest dollar.
                    }),
                },
                { x: "fundType", y: "total", text: "total", dy: -10 }
            )),
            oPlot.textY( //this section gets us our individual bars (by PRA) in each vertical stacked bar
                tidy,
                oPlot.stackY(
                    {
                        x: "fundType",
                        z: "pra",
                        y: "total",
                        text: (d) => (d.total < 50 ? null :
                            (d.total).toLocaleString("en-US", {
                                minimumFractionDigits: 0,
                                maximumFractionDigits: 0
                            })
                        ),
                    }
                )
            )
        ]
    });

As a follow-up to this, is there any way to base the text filter on the y2-y1 height inside of the stackY transform instead of a hardcoded value (in this case 50)? I want to hide the text if there are not enough pixels for it to fit.

Maybe something like this:
https://observablehq.com/@recifs/plot-filtering-out-small-values

Apply Plot.filter after the values have been stacked. But I’m not sure it answers your question because the threshold is in data space (unscaled value), not in pixels.

Thanks for the response. I am using data space now, but it would be nice if I could access scaled space to determine the threshold value in pixels. If not, I will have to calculate the threshold in data space for each chart. I am just trying to use the simplest approach and if plot already supports it, I want to use it. I just cannot find any examples of it in the documentation.