layerchart
layerchart copied to clipboard
Is this a proper way to create a stacked bar chart both with negative and positive values?
I was trying to create a stacked bar chart with both positive and negative values (on opposite sides of the x-axis) and ended up doing the following, which works, but makes me wonder if perhaps I am missing something and it could have been done more easily?
<script>
import { extent } from "d3-array";
import { scaleBand, scaleOrdinal } from "d3-scale";
import { stack, stackOffsetDiverging } from "d3-shape";
import { format, parseISO } from "date-fns";
import {
Axis,
Bars,
Chart,
Rule,
Svg,
Tooltip,
TooltipItem,
pivotLonger,
pivotWider,
} from "layerchart";
import { PeriodType, formatDate } from "svelte-ux";
/**
* Creates stack data from a dataset with options for keys and stacking behavior.
*/
export function createStackData(data, { xKey, stackBy }) {
// Stack only
const pivotData = pivotWider(data, xKey, stackBy, "value");
const stackKeys = [...new Set(data.map((d) => d[stackBy]))];
const stackData = stack().keys(stackKeys).offset(stackOffsetDiverging)(
pivotData
);
const result = stackData.flatMap((series) => {
return series.flatMap((s) => {
return {
...s.data,
keys: [s.data[xKey], series.key],
values: [s[0], s[1]],
};
});
});
return result;
}
export let stackedData = [
{
delivery_datetime: "2024-03-27T00:00:00Z",
delivered: 18,
good_return: 0,
bad_return: -3,
},
{
delivery_datetime: "2024-03-26T00:00:00Z",
delivered: 4,
good_return: 0,
bad_return: 0,
},
].map((d) => ({
...d,
delivery_datetime: parseISO(d.delivery_datetime),
}));
let myLongData = pivotLonger(
stackedData,
["delivered", "good_return", "bad_return"],
"type",
"value"
);
const stackedSeperatedData = createStackData(myLongData, {
xKey: "delivery_datetime",
stackBy: "type",
});
const myColorKeys = [...new Set(myLongData.map((x) => x.type))];
const myKeyColors = [
"hsl(142, 100%, 51%)", // Success color
"hsl(1, 68%, 49%)", // Warning color
"hsl(0, 100%, 50%)", // Danger color
];
console.log(
"extent(stackedSeperatedData.flatMap((d) => d.values))",
extent(stackedSeperatedData.flatMap((d) => d.values))
);
</script>
<Chart
data={stackedSeperatedData}
x="delivery_datetime"
xScale={scaleBand().paddingInner(0.4).paddingOuter(0.1)}
y="values"
yBaseline={0}
yDomain={extent(stackedSeperatedData.flatMap((d) => d.values))}
yNice={4}
yPadding={[16, 16]}
r={(d) => d.keys[1]}
rScale={scaleOrdinal()}
rDomain={myColorKeys}
rRange={myKeyColors}
padding={{ left: 16, bottom: 24 }}
tooltip={{ mode: "band" }}
>
<Svg>
<Axis placement="left" grid rule />
<Axis
placement="bottom"
format={(d) => formatDate(d, PeriodType.Day, { variant: "short" })}
rule
/>
<Rule y={0} stroke="currentColor" strokeWidth={1} />
<Bars radius={1} strokeWidth={1} />
</Svg>
<Tooltip
header={(data) => format(data.delivery_datetime, "eee, MMMM do")}
let:data
>
<TooltipItem label="delivered" value={data.delivered} />
<TooltipItem label="good returns" value={Math.abs(data.good_return)} />
<TooltipItem label="bad returns" value={Math.abs(data.bad_return)} />
</Tooltip>
</Chart>
In any case, perhaps this code can be useful to someone else...
Hey @leomorpho, thanks for checking out LayerChart. It looks like you pulled part of createStackData() from utils/stack. Was there something it was lacking (I didn't spot any differences after a quick glance)
Regardless, nothing jumps out at me that could be simplified ATM, but I would like to improve grouped/stacked at some point, especially regarding tooltip/data. See https://github.com/techniq/layerchart/issues/21 and https://github.com/techniq/layerchart/issues/98.
I would also like grouped scales to be more integrated to <Chart>, which are really just derived scales (x1Scale derives from xScale and uses it's bandwidth instead of the chart's width). Had a chat about adding something in LayerCake, but wasn't worth it at that point (needed more investigation).
However it might be improved, I want to make sure it supports switching between grouped only, stacked only, and group and stacked, such as this example.
Speaking of examples, it would be good to add a diverging bar chart example to the docs., similar to what you have (and both stacked and simpler single bar examples), similar to these:
- https://observablehq.com/@d3/diverging-bar-chart
- https://observablehq.com/@d3/diverging-stacked-bar-chart
- https://observablehq.com/@observablehq/plot-diverging-stacked-bar
Sorry for the delay, but take a look at the new BarChart with series support. This can not be accomplished with:
<div class="h-[300px] p-4 border rounded">
<BarChart
data={wideData}
x="year"
series={[
{
key: "apples",
value: (d) => -d.apples,
color: "hsl(var(--color-danger))",
},
{
key: "bananas",
color: "hsl(var(--color-warning))",
},
{
key: "cherries",
color: "hsl(var(--color-success))",
},
{
key: "grapes",
color: "hsl(var(--color-info))",
},
]}
seriesLayout="stackDiverging"
props={{
xAxis: { format: "none" },
yAxis: { format: "metric" },
}}
/>
</div>