Home > front end >  Create a generic function that creates stacked dataset using d3
Create a generic function that creates stacked dataset using d3

Time:11-24

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

CodePudding user response:

I tried to make a more generic approach as you suggest mixing the two functions. By default, seems like your getStackedSeries function does not need to know about date and category properties, you can use a Generic Type to ensure just the value property, as we need to know that to calculate start and end values.

The full implementation can be viewed here on codesandbox.

export function getStackedSeries<T extends Datum>(
  data: T[],
  groupByProperty: PropertyType<T>
) {
  const groupedData = groupBy(data, (d) => d[groupByProperty]);

  const acumulatedData = Object.entries(groupedData).flatMap(
    ([_, groupedValue]) => {
      let acumulator = 0;

      return groupedValue.map(({ value, ...rest }) => {
        const obj = {
          ...rest,
          value: value,
          start: acumulator,
          end: acumulator   value
        };

        acumulator  = value;

        return obj;
      });
    }
  );

  return acumulatedData;
}

The getStackedSeries() now receives a data property that extends Datum type, which is:

export interface Datum {
  value: number;
}

With that and a second property called groupByProperty we can define the groupBy clause and return all flatten by flatMap.

You probably notice that the return type now is defined by typescript dynamically by the use of a generic <T>. For example:

const dataGroupedByDate: (Omit<{
    date: string;
    category: string;
    value: number;
}, "value"> & {
    value: number;
    start: number;
    end: number;
})[]

You can also type this part of the function, but makes sense to let the compiler works for you and generate the types automatically based on input.

CodePudding user response:

You could group by date for start/end and take another grouping by category for the result set.

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 }],
    result = Object
        .values(dataset
            .reduce((r, { date, category, value }) => {
                const
                    start = r.date[date]?.at(-1).end ?? 0,
                    end = start   value,
                    object = { date, category, value, start, end };

                (r.date[date] ??= []).push(object);
                (r.category[category] ??= []).push(object);
                return r;
            }, { date: {}, category: {} })
            .category
        )
        .flat();

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

  • Related