techniq / layerchart

Composable Svelte chart components to build a wide range of visualizations
https://www.layerchart.com
MIT License
668 stars 12 forks source link

Is this a proper way to create a stacked bar chart both with negative and positive values? #140

Closed leomorpho closed 1 month ago

leomorpho commented 7 months ago

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?

Screenshot 2024-04-18 at 17 11 06
<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...

techniq commented 7 months ago

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:

techniq commented 1 month ago

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>

image