У меня есть этот набор данных:
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 }
];
И мне нужно создать гистограмму с накоплением. Для этого я использовал функцию d3 stack(). Результат, который мне нужен, таков:
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 }
]
Итак, те же данные, но со свойствами start и end, вычисленными d3.
Я создал функцию, которая принимает ввод dataset и возвращает 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>>;
}
Как видите, функции специфичны для типа Datum. Есть ли способ изменить их, чтобы они работали для универсального типа T, который имеет как минимум три поля date, category, value?
Я имею в виду, что я хотел бы иметь что-то вроде этого:
interface StackedStartEnd {
start: number
end: number
}
function getStackedSeries<T>(dataset: T[]): T extends StackedStartEnd
Очевидно, что этот фрагмент кода следует реорганизовать, чтобы сделать его более универсальным:
{
[DATE]: ...,
[CATEGORY]: ...,
[VALUE]: ...,
start: ...,
end: ...,
}
Вот рабочий код.
Я не эксперт по TypeScript, поэтому мне нужна помощь. Честно говоря, я пытался изменить сигнатуру функции, но мне это не удалось, и в любом случае я хотел бы сделать функции как можно более общими, и я не знаю, с чего начать. Нужно ли передавать функциям имена используемых столбцов?
Большое спасибо
Я попытался сделать более общий подход, поскольку вы предлагаете смешивать две функции. По умолчанию кажется, что вашей функции getStackedSeries не нужно знать о свойствах date и category, вы можете использовать универсальный тип, чтобы обеспечить только свойство value, так как нам нужно знать это для вычисления значений start и end.
Полную реализацию можно посмотреть здесь на codeandbox.
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;
}
GetStackedSeries() теперь получает свойство data, которое расширяет тип Datum, а именно:
export interface Datum {
value: number;
}
С помощью этого и второго свойства с именем groupByProperty мы можем определить предложение groupBy и вернуть все сглаживание по flatMap.
Вы, наверное, заметили, что тип возвращаемого значения теперь определяется машинописным текстом динамически с использованием универсального <T>. Например:
const dataGroupedByDate: (Omit<{
date: string;
category: string;
value: number;
}, "value"> & {
value: number;
start: number;
end: number;
})[]
Вы также можете ввести эту часть функции, но имеет смысл позволить компилятору работать за вас и автоматически генерировать типы на основе ввода.
Вы можете сгруппировать по дате начала/окончания и выбрать другую группировку по категориям для набора результатов.
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.info(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }