[DASHBOARDS] Add cumulative setting for bar chart and line chart (#16248)
## Description - Add cumulative setting - This setting is only present for dates - It is preserved when switching from a date field to another date field but it is reset when switching to another field type ## Video QA https://github.com/user-attachments/assets/a070bf54-8ef6-4e35-9378-a514fe1c3848 https://github.com/user-attachments/assets/07edfe87-253c-45a5-b582-06f2df9a2dfc
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`applyCumulativeTransformToBarChartData should handle basic accumulation 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "a",
|
||||
"value": 10,
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"value": 30,
|
||||
},
|
||||
{
|
||||
"id": "c",
|
||||
"value": 60,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToBarChartData should handle empty data 1`] = `[]`;
|
||||
|
||||
exports[`applyCumulativeTransformToBarChartData should handle filter above rangeMax 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "a",
|
||||
"value": 10,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToBarChartData should handle filter below rangeMin 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "b",
|
||||
"value": 20,
|
||||
},
|
||||
{
|
||||
"id": "c",
|
||||
"value": 30,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToBarChartData should handle skip non-numeric values 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "a",
|
||||
"value": 10,
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"value": 10,
|
||||
},
|
||||
{
|
||||
"id": "c",
|
||||
"value": 30,
|
||||
},
|
||||
]
|
||||
`;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`applyCumulativeTransformToLineChartData should handle basic accumulation 1`] = `
|
||||
[
|
||||
{
|
||||
"x": "Jan",
|
||||
"y": 10,
|
||||
},
|
||||
{
|
||||
"x": "Feb",
|
||||
"y": 30,
|
||||
},
|
||||
{
|
||||
"x": "Mar",
|
||||
"y": 60,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToLineChartData should handle empty data 1`] = `[]`;
|
||||
|
||||
exports[`applyCumulativeTransformToLineChartData should handle filter above rangeMax 1`] = `
|
||||
[
|
||||
{
|
||||
"x": "a",
|
||||
"y": 10,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToLineChartData should handle filter below rangeMin 1`] = `
|
||||
[
|
||||
{
|
||||
"x": "b",
|
||||
"y": 20,
|
||||
},
|
||||
{
|
||||
"x": "c",
|
||||
"y": 30,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToLineChartData should handle skip null y values 1`] = `
|
||||
[
|
||||
{
|
||||
"x": "a",
|
||||
"y": 10,
|
||||
},
|
||||
{
|
||||
"x": "b",
|
||||
"y": 10,
|
||||
},
|
||||
{
|
||||
"x": "c",
|
||||
"y": 30,
|
||||
},
|
||||
]
|
||||
`;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle accumulate each key independently 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "Jan",
|
||||
"revenue": 100,
|
||||
"sales": 10,
|
||||
},
|
||||
{
|
||||
"id": "Feb",
|
||||
"revenue": 300,
|
||||
"sales": 30,
|
||||
},
|
||||
{
|
||||
"id": "Mar",
|
||||
"revenue": 600,
|
||||
"sales": 60,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle empty data 1`] = `[]`;
|
||||
|
||||
exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle filter above rangeMax based on total sum 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "a",
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle filter below rangeMin based on total sum 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "c",
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`applyCumulativeTransformToTwoDimensionalBarChartData should handle skip non-numeric values 1`] = `
|
||||
[
|
||||
{
|
||||
"id": "a",
|
||||
"x": 10,
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"x": 10,
|
||||
},
|
||||
{
|
||||
"id": "c",
|
||||
"x": 30,
|
||||
},
|
||||
]
|
||||
`;
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { applyCumulativeTransformToBarChartData } from '../applyCumulativeTransformToBarChartData';
|
||||
|
||||
describe('applyCumulativeTransformToBarChartData', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'basic accumulation',
|
||||
data: [
|
||||
{ id: 'a', value: 10 },
|
||||
{ id: 'b', value: 20 },
|
||||
{ id: 'c', value: 30 },
|
||||
],
|
||||
aggregateKey: 'value',
|
||||
},
|
||||
{
|
||||
name: 'filter below rangeMin',
|
||||
data: [
|
||||
{ id: 'a', value: 10 },
|
||||
{ id: 'b', value: 10 },
|
||||
{ id: 'c', value: 10 },
|
||||
],
|
||||
aggregateKey: 'value',
|
||||
rangeMin: 15,
|
||||
},
|
||||
{
|
||||
name: 'filter above rangeMax',
|
||||
data: [
|
||||
{ id: 'a', value: 10 },
|
||||
{ id: 'b', value: 20 },
|
||||
{ id: 'c', value: 30 },
|
||||
],
|
||||
aggregateKey: 'value',
|
||||
rangeMax: 25,
|
||||
},
|
||||
{
|
||||
name: 'empty data',
|
||||
data: [],
|
||||
aggregateKey: 'value',
|
||||
},
|
||||
{
|
||||
name: 'skip non-numeric values',
|
||||
data: [
|
||||
{ id: 'a', value: 10 },
|
||||
{ id: 'b', value: 'not a number' },
|
||||
{ id: 'c', value: 20 },
|
||||
],
|
||||
aggregateKey: 'value',
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
'should handle $name',
|
||||
({ data, aggregateKey, rangeMin, rangeMax }) => {
|
||||
const result = applyCumulativeTransformToBarChartData({
|
||||
data,
|
||||
aggregateKey,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { applyCumulativeTransformToLineChartData } from '../applyCumulativeTransformToLineChartData';
|
||||
|
||||
describe('applyCumulativeTransformToLineChartData', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'basic accumulation',
|
||||
data: [
|
||||
{ x: 'Jan', y: 10 },
|
||||
{ x: 'Feb', y: 20 },
|
||||
{ x: 'Mar', y: 30 },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'filter below rangeMin',
|
||||
data: [
|
||||
{ x: 'a', y: 10 },
|
||||
{ x: 'b', y: 10 },
|
||||
{ x: 'c', y: 10 },
|
||||
],
|
||||
rangeMin: 15,
|
||||
},
|
||||
{
|
||||
name: 'filter above rangeMax',
|
||||
data: [
|
||||
{ x: 'a', y: 10 },
|
||||
{ x: 'b', y: 20 },
|
||||
{ x: 'c', y: 30 },
|
||||
],
|
||||
rangeMax: 25,
|
||||
},
|
||||
{
|
||||
name: 'empty data',
|
||||
data: [],
|
||||
},
|
||||
{
|
||||
name: 'skip null y values',
|
||||
data: [
|
||||
{ x: 'a', y: 10 },
|
||||
{ x: 'b', y: null },
|
||||
{ x: 'c', y: 20 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)('should handle $name', ({ data, rangeMin, rangeMax }) => {
|
||||
const result = applyCumulativeTransformToLineChartData({
|
||||
data,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { applyCumulativeTransformToTwoDimensionalBarChartData } from '../applyCumulativeTransformToTwoDimensionalBarChartData';
|
||||
|
||||
describe('applyCumulativeTransformToTwoDimensionalBarChartData', () => {
|
||||
const testCases = [
|
||||
{
|
||||
name: 'accumulate each key independently',
|
||||
data: [
|
||||
{ id: 'Jan', sales: 10, revenue: 100 },
|
||||
{ id: 'Feb', sales: 20, revenue: 200 },
|
||||
{ id: 'Mar', sales: 30, revenue: 300 },
|
||||
],
|
||||
keys: ['sales', 'revenue'],
|
||||
},
|
||||
{
|
||||
name: 'filter below rangeMin based on total sum',
|
||||
data: [
|
||||
{ id: 'a', x: 10, y: 10 },
|
||||
{ id: 'b', x: 10, y: 10 },
|
||||
{ id: 'c', x: 10, y: 10 },
|
||||
],
|
||||
keys: ['x', 'y'],
|
||||
rangeMin: 50,
|
||||
},
|
||||
{
|
||||
name: 'filter above rangeMax based on total sum',
|
||||
data: [
|
||||
{ id: 'a', x: 10, y: 10 },
|
||||
{ id: 'b', x: 20, y: 20 },
|
||||
{ id: 'c', x: 30, y: 30 },
|
||||
],
|
||||
keys: ['x', 'y'],
|
||||
rangeMax: 50,
|
||||
},
|
||||
{
|
||||
name: 'empty data',
|
||||
data: [],
|
||||
keys: ['x', 'y'],
|
||||
},
|
||||
{
|
||||
name: 'skip non-numeric values',
|
||||
data: [
|
||||
{ id: 'a', x: 10 },
|
||||
{ id: 'b', x: 'not a number' },
|
||||
{ id: 'c', x: 20 },
|
||||
],
|
||||
keys: ['x'],
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
'should handle $name',
|
||||
({ data, keys, rangeMin, rangeMax }) => {
|
||||
const result = applyCumulativeTransformToTwoDimensionalBarChartData({
|
||||
data,
|
||||
keys,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type ApplyCumulativeTransformToBarChartDataOptions = {
|
||||
data: BarDatum[];
|
||||
aggregateKey: string;
|
||||
rangeMin?: number;
|
||||
rangeMax?: number;
|
||||
};
|
||||
|
||||
export const applyCumulativeTransformToBarChartData = ({
|
||||
data,
|
||||
aggregateKey,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
}: ApplyCumulativeTransformToBarChartDataOptions): BarDatum[] => {
|
||||
const { result } = data.reduce<{ result: BarDatum[]; runningTotal: number }>(
|
||||
(accumulator, datum) => {
|
||||
const value = datum[aggregateKey];
|
||||
|
||||
if (typeof value === 'number') {
|
||||
accumulator.runningTotal += value;
|
||||
}
|
||||
|
||||
const cumulativeValue = accumulator.runningTotal;
|
||||
|
||||
const isOutOfRange =
|
||||
(isDefined(rangeMin) && cumulativeValue < rangeMin) ||
|
||||
(isDefined(rangeMax) && cumulativeValue > rangeMax);
|
||||
|
||||
if (!isOutOfRange) {
|
||||
accumulator.result.push({ ...datum, [aggregateKey]: cumulativeValue });
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
},
|
||||
{ result: [], runningTotal: 0 },
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type ApplyCumulativeTransformToLineChartDataOptions = {
|
||||
data: LineChartDataPoint[];
|
||||
rangeMin?: number;
|
||||
rangeMax?: number;
|
||||
};
|
||||
|
||||
export const applyCumulativeTransformToLineChartData = ({
|
||||
data,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
}: ApplyCumulativeTransformToLineChartDataOptions): LineChartDataPoint[] => {
|
||||
const { result } = data.reduce<{
|
||||
result: LineChartDataPoint[];
|
||||
runningTotal: number;
|
||||
}>(
|
||||
(accumulator, point) => {
|
||||
if (point.y !== null) {
|
||||
accumulator.runningTotal += point.y;
|
||||
}
|
||||
|
||||
const cumulativeValue = accumulator.runningTotal;
|
||||
|
||||
const isOutOfRange =
|
||||
(isDefined(rangeMin) && cumulativeValue < rangeMin) ||
|
||||
(isDefined(rangeMax) && cumulativeValue > rangeMax);
|
||||
|
||||
if (!isOutOfRange) {
|
||||
accumulator.result.push({ ...point, y: cumulativeValue });
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
},
|
||||
{ result: [], runningTotal: 0 },
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type ApplyCumulativeTransformToTwoDimensionalBarChartDataOptions = {
|
||||
data: BarDatum[];
|
||||
keys: string[];
|
||||
rangeMin?: number;
|
||||
rangeMax?: number;
|
||||
};
|
||||
|
||||
export const applyCumulativeTransformToTwoDimensionalBarChartData = ({
|
||||
data,
|
||||
keys,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
}: ApplyCumulativeTransformToTwoDimensionalBarChartDataOptions): BarDatum[] => {
|
||||
const { result } = data.reduce<{
|
||||
result: BarDatum[];
|
||||
runningTotals: Record<string, number>;
|
||||
}>(
|
||||
(accumulator, datum) => {
|
||||
const newDatum = { ...datum };
|
||||
|
||||
for (const key of keys) {
|
||||
const value = datum[key];
|
||||
|
||||
if (typeof value === 'number') {
|
||||
accumulator.runningTotals[key] += value;
|
||||
}
|
||||
|
||||
newDatum[key] = accumulator.runningTotals[key];
|
||||
}
|
||||
|
||||
const totalValue = keys.reduce((sum, key) => {
|
||||
const value = newDatum[key];
|
||||
return sum + (typeof value === 'number' ? value : 0);
|
||||
}, 0);
|
||||
|
||||
const isOutOfRange =
|
||||
(isDefined(rangeMin) && totalValue < rangeMin) ||
|
||||
(isDefined(rangeMax) && totalValue > rangeMax);
|
||||
|
||||
if (!isOutOfRange) {
|
||||
accumulator.result.push(newDatum);
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
},
|
||||
{
|
||||
result: [],
|
||||
runningTotals: Object.fromEntries(keys.map((key) => [key, 0])),
|
||||
},
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
+6
-2
@@ -89,8 +89,12 @@ export const transformGroupByDataToLineChartData = ({
|
||||
const filteredResults = filterGroupByResults({
|
||||
rawResults,
|
||||
filterOptions: {
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
rangeMin: configuration.isCumulative
|
||||
? undefined
|
||||
: (configuration.rangeMin ?? undefined),
|
||||
rangeMax: configuration.isCumulative
|
||||
? undefined
|
||||
: (configuration.rangeMax ?? undefined),
|
||||
omitNullValues: configuration.omitNullValues ?? false,
|
||||
},
|
||||
aggregateField,
|
||||
|
||||
+11
-2
@@ -8,10 +8,11 @@ import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLin
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData';
|
||||
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type LineChartConfiguration } from '~/generated/graphql';
|
||||
|
||||
@@ -91,12 +92,20 @@ export const transformOneDimensionalGroupByToLineChartData = ({
|
||||
})
|
||||
.filter((point) => isDefined(point));
|
||||
|
||||
const transformedData = configuration.isCumulative
|
||||
? applyCumulativeTransformToLineChartData({
|
||||
data,
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
})
|
||||
: data;
|
||||
|
||||
const series: LineChartSeries[] = [
|
||||
{
|
||||
id: aggregateField.name,
|
||||
label: aggregateField.label,
|
||||
color: (configuration.color ?? GRAPH_DEFAULT_COLOR) as GraphColor,
|
||||
data,
|
||||
data: transformedData,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+11
-2
@@ -7,10 +7,11 @@ import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLin
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
|
||||
import { type RawDimensionValue } from '@/page-layout/widgets/graph/types/RawDimensionValue';
|
||||
import { applyCumulativeTransformToLineChartData } from '@/page-layout/widgets/graph/utils/applyCumulativeTransformToLineChartData';
|
||||
import { buildFormattedToRawLookup } from '@/page-layout/widgets/graph/utils/buildFormattedToRawLookup';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { computeAggregateValueFromGroupByResult } from '@/page-layout/widgets/graph/utils/computeAggregateValueFromGroupByResult';
|
||||
import { formatDimensionValue } from '@/page-layout/widgets/graph/utils/formatDimensionValue';
|
||||
import { formatPrimaryDimensionValues } from '@/page-layout/widgets/graph/utils/formatPrimaryDimensionValues';
|
||||
import { sortLineChartSeries } from '@/page-layout/widgets/graph/utils/sortLineChartSeries';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type LineChartConfiguration } from '~/generated/graphql';
|
||||
@@ -117,11 +118,19 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
y: xToYMap.get(xValue) ?? 0,
|
||||
}));
|
||||
|
||||
const transformedData = configuration.isCumulative
|
||||
? applyCumulativeTransformToLineChartData({
|
||||
data,
|
||||
rangeMin: configuration.rangeMin ?? undefined,
|
||||
rangeMax: configuration.rangeMax ?? undefined,
|
||||
})
|
||||
: data;
|
||||
|
||||
return {
|
||||
id: seriesKey,
|
||||
label: seriesKey,
|
||||
color: configuration.color as GraphColor,
|
||||
data,
|
||||
data: transformedData,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user