Sunday, 20 November 2022

Create a generic function that creates stacked dataset using d3

I have this dataset:

const dataset = [
  { date: "2022-01-01", category: "red", value: 10 },
  { date: "2022-01-01", category: "blue", value: 20 },
  { date: "2022-01-01", category: "gold", value: 30 },
  { date: "2022-01-01", category: "green", value: 40 },
  { date: "2022-01-02", category: "red", value: 5 },
  { date: "2022-01-02", category: "blue", value: 15 },
  { date: "2022-01-02", category: "gold", value: 25 },
  { date: "2022-01-02", category: "green", value: 35 }
];

And I need to create a stacked barchart. To do that I used the d3 stack() function. The result I need is this:

const stackedDataset = [
  { date: "2022-01-01", category: "red", value: 10, start: 0, end: 10 },
  { date: "2022-01-02", category: "red", value: 5, start: 0, end: 5 },
  { date: "2022-01-01", category: "blue", value: 20, start: 10, end: 30 },
  { date: "2022-01-02", category: "blue", value: 15, start: 5, end: 20 },
  { date: "2022-01-01", category: "gold", value: 30, start: 30, end: 60 },
  { date: "2022-01-02", category: "gold", value: 25, start: 20, end: 45 },
  { date: "2022-01-01", category: "green", value: 40, start: 60, end: 100 },
  { date: "2022-01-02", category: "green", value: 35, start: 45, end: 80 }
]

So the same data but with a start and end property computed by d3.

I created a function that takes in input dataset and returns stackedDataset:

export function getStackedSeries(dataset: Datum[]) {
  const categories = uniq(dataset.map((d) => d[CATEGORY])) as string[];
  const datasetGroupedByDateFlat = flatDataset(dataset);
  const stackGenerator = d3.stack().keys(categories);
  const seriesRaw = stackGenerator(
    datasetGroupedByDateFlat as Array<Dictionary<number>>
  );
  const series = seriesRaw.flatMap((serie, si) => {
    const category = categories[si];
    const result = serie.map((s, sj) => {
      return {
        [DATE]: datasetGroupedByDateFlat[sj][DATE] as string,
        [CATEGORY]: category,
        [VALUE]: datasetGroupedByDateFlat[sj][category] as number,
        start: s[0] || 0,
        end: s[1] || 0
      };
    });
    return result;
  });
  return series;
}

export function flatDataset(
  dataset: Datum[]
): Array<Dictionary<string | number>> {
  if (dataset.length === 0 || !DATE) {
    return (dataset as unknown) as Array<Dictionary<string | number>>;
  }
  const columnToBeFlatValues = uniqBy(dataset, CATEGORY).map(
    (d) => d[CATEGORY]
  );
  const datasetGroupedByDate = groupBy(dataset, DATE);
  const datasetGroupedByMainCategoryFlat = Object.entries(
    datasetGroupedByDate
  ).map(([date, datasetForDate]) => {
    const categoriesObject = columnToBeFlatValues.reduce((acc, value) => {
      const datum = datasetForDate.find(
        (d) => d[DATE] === date && d[CATEGORY] === value
      );
      acc[value] = datum?.[VALUE];
      return acc;
    }, {} as Dictionary<string | number | undefined>);
    return {
      [DATE]: date,
      ...categoriesObject
    };
  });
  return datasetGroupedByMainCategoryFlat as Array<Dictionary<string | number>>;
}

As you can see, the functions are specific for Datum type. Is there a way to modify them to make them works for a generic type T that has at least the three fields date, category, value?

I mean, I would like to have something like this:

interface StackedStartEnd {
  start: number
  end: number
}

function getStackedSeries<T>(dataset: T[]): T extends StackedStartEnd

Obviously this piece of code should be refactored to make it more generic:

{
  [DATE]: ...,
  [CATEGORY]: ...,
  [VALUE]: ...,
  start: ...,
  end: ...,
}

Here the working code.

I'm not a TypeScript expert so I need some help. Honestly what I tried to do was to modify the function signature but I failed and, anyway, I would like to make the functions as generic as possible and I don't know how to start. Do I need to pass to the functions also the used columns names?

Thank you very much



from Create a generic function that creates stacked dataset using d3

No comments:

Post a Comment