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; }