[DASHBOARDS] Rotate ticks on bar and line charts (#16528)
## Description 3 steps depending on the widget width. From bigger to tighter space: - Fully shown horizontal text - Rotated text - Rotated text with skipped ticks to avoid overlapping Also created common files for all constants for bar and pie charts. Since a lot of them are shared, they can be inherited from a common file. ## Video QA https://github.com/user-attachments/assets/fd58d412-1a8b-4bd6-a420-4c03767e98d5
This commit is contained in:
+57
@@ -1,3 +1,4 @@
|
||||
import { ROTATION_THRESHOLD_WIDTH } from '@/page-layout/widgets/graph/constants/RotationThresholdWidth';
|
||||
import { computeChartCategoryTickValues } from '../computeChartCategoryTickValues';
|
||||
|
||||
describe('computeChartCategoryTickValues', () => {
|
||||
@@ -89,4 +90,60 @@ describe('computeChartCategoryTickValues', () => {
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
describe('with widthPerTick parameter', () => {
|
||||
it('should return all values when widthPerTick is above rotation threshold', () => {
|
||||
const values = ['A', 'B', 'C', 'D', 'E'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 200,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
widthPerTick: ROTATION_THRESHOLD_WIDTH + 10,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['A', 'B', 'C', 'D', 'E']);
|
||||
});
|
||||
|
||||
it('should return all values when rotated and widthPerTick exceeds minimumSizePerTick', () => {
|
||||
const values = ['A', 'B', 'C', 'D', 'E'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 200,
|
||||
minimumSizePerTick: 20,
|
||||
values,
|
||||
widthPerTick: 30,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['A', 'B', 'C', 'D', 'E']);
|
||||
});
|
||||
|
||||
it('should omit ticks when rotated and widthPerTick is below minimumSizePerTick', () => {
|
||||
const values = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
||||
const result = computeChartCategoryTickValues({
|
||||
availableSize: 400,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
widthPerTick: 10,
|
||||
});
|
||||
|
||||
expect(result.length).toBe(4);
|
||||
expect(result).toEqual(['A', 'C', 'F', 'H']);
|
||||
});
|
||||
|
||||
it('should behave like no widthPerTick when widthPerTick is undefined', () => {
|
||||
const values = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
||||
const resultWithoutWidthPerTick = computeChartCategoryTickValues({
|
||||
availableSize: 400,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
});
|
||||
const resultWithUndefined = computeChartCategoryTickValues({
|
||||
availableSize: 400,
|
||||
minimumSizePerTick: 100,
|
||||
values,
|
||||
widthPerTick: undefined,
|
||||
});
|
||||
|
||||
expect(resultWithoutWidthPerTick).toEqual(resultWithUndefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { computeMaxLabelLengthForMargin } from '../computeMaxLabelLengthForMargin';
|
||||
|
||||
describe('computeMaxLabelLengthForMargin', () => {
|
||||
it('should calculate label length based on margin size and font size', () => {
|
||||
const result = computeMaxLabelLengthForMargin({
|
||||
marginSize: 100,
|
||||
axisFontSize: 11,
|
||||
});
|
||||
|
||||
const expectedCharacterWidth =
|
||||
11 * COMMON_CHART_CONSTANTS.HORIZONTAL_LABEL_CHARACTER_WIDTH_RATIO;
|
||||
const availableWidth = 100 - COMMON_CHART_CONSTANTS.TICK_PADDING_ALLOWANCE;
|
||||
const expectedLength = Math.floor(availableWidth / expectedCharacterWidth);
|
||||
|
||||
expect(result).toBe(expectedLength);
|
||||
});
|
||||
|
||||
it('should return minimum length when margin is too small', () => {
|
||||
const result = computeMaxLabelLengthForMargin({
|
||||
marginSize: 20,
|
||||
axisFontSize: 11,
|
||||
});
|
||||
|
||||
const minimumLength =
|
||||
COMMON_CHART_CONSTANTS.TICK_MINIMUM_NUMBER_OF_DISPLAYED_CHARACTERS;
|
||||
|
||||
expect(result).toBe(minimumLength);
|
||||
});
|
||||
|
||||
it('should scale with font size', () => {
|
||||
const smallFont = computeMaxLabelLengthForMargin({
|
||||
marginSize: 100,
|
||||
axisFontSize: 10,
|
||||
});
|
||||
|
||||
const largeFont = computeMaxLabelLengthForMargin({
|
||||
marginSize: 100,
|
||||
axisFontSize: 14,
|
||||
});
|
||||
|
||||
expect(smallFont).toBeGreaterThan(largeFont);
|
||||
});
|
||||
|
||||
it('should scale with margin size', () => {
|
||||
const smallMargin = computeMaxLabelLengthForMargin({
|
||||
marginSize: 50,
|
||||
axisFontSize: 11,
|
||||
});
|
||||
|
||||
const largeMargin = computeMaxLabelLengthForMargin({
|
||||
marginSize: 150,
|
||||
axisFontSize: 11,
|
||||
});
|
||||
|
||||
expect(largeMargin).toBeGreaterThan(smallMargin);
|
||||
});
|
||||
|
||||
it('should never return less than the minimum displayable characters', () => {
|
||||
const result = computeMaxLabelLengthForMargin({
|
||||
marginSize: 0,
|
||||
axisFontSize: 11,
|
||||
});
|
||||
|
||||
const minimumLength =
|
||||
COMMON_CHART_CONSTANTS.TICK_MINIMUM_NUMBER_OF_DISPLAYED_CHARACTERS;
|
||||
|
||||
expect(result).toBeGreaterThanOrEqual(minimumLength);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity';
|
||||
import { CalendarStartDay } from 'twenty-shared';
|
||||
import {
|
||||
type FirstDayOfTheWeek,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { type AggregateOperations } from '@/object-record/record-table/constants
|
||||
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
|
||||
import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOptions';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { GRAPH_DEFAULT_AGGREGATE_VALUE } from '@/page-layout/widgets/graph/constants/GraphDefaultAggregateValue.constant';
|
||||
import { GRAPH_DEFAULT_AGGREGATE_VALUE } from '@/page-layout/widgets/graph/constants/GraphDefaultAggregateValue';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
+13
@@ -1,18 +1,31 @@
|
||||
import { ROTATION_THRESHOLD_WIDTH } from '@/page-layout/widgets/graph/constants/RotationThresholdWidth';
|
||||
import { computeCategoryTickValues } from '@/page-layout/widgets/graph/utils/computeCategoryTickValues';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const computeChartCategoryTickValues = ({
|
||||
availableSize,
|
||||
minimumSizePerTick,
|
||||
values,
|
||||
widthPerTick,
|
||||
}: {
|
||||
availableSize: number;
|
||||
minimumSizePerTick: number;
|
||||
values: (string | number)[];
|
||||
widthPerTick?: number;
|
||||
}): (string | number)[] => {
|
||||
if (availableSize <= 0 || values.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isDefined(widthPerTick)) {
|
||||
const willRotate = widthPerTick < ROTATION_THRESHOLD_WIDTH;
|
||||
const shouldOmitTicks = willRotate && widthPerTick < minimumSizePerTick;
|
||||
|
||||
if (!shouldOmitTicks) {
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
const numberOfTicks = Math.floor(availableSize / minimumSizePerTick);
|
||||
const tickIndices = computeCategoryTickValues(numberOfTicks, values.length);
|
||||
|
||||
|
||||
+6
-6
@@ -1,8 +1,6 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const POSITIVE_RANGE_PADDING_RATIO = 0.1;
|
||||
const MINIMUM_POSITIVE_RANGE_PADDING = 1;
|
||||
|
||||
type ComputeEffectiveValueRangeParams = {
|
||||
calculatedMinimum: number;
|
||||
calculatedMaximum: number;
|
||||
@@ -39,8 +37,9 @@ export const computeEffectiveValueRange = ({
|
||||
? positiveRangePaddingTarget
|
||||
: positiveRangePaddingTarget +
|
||||
Math.max(
|
||||
Math.abs(positiveRangePaddingTarget) * POSITIVE_RANGE_PADDING_RATIO,
|
||||
MINIMUM_POSITIVE_RANGE_PADDING,
|
||||
Math.abs(positiveRangePaddingTarget) *
|
||||
COMMON_CHART_CONSTANTS.POSITIVE_RANGE_PADDING_RATIO,
|
||||
COMMON_CHART_CONSTANTS.MINIMUM_POSITIVE_RANGE_PADDING,
|
||||
);
|
||||
|
||||
let effectiveMinimumValue = baseMinimumValue;
|
||||
@@ -49,7 +48,8 @@ export const computeEffectiveValueRange = ({
|
||||
if (!isDefined(rangeMax) && !isDefined(rangeMin)) {
|
||||
if (effectiveMinimumValue === effectiveMaximumValue) {
|
||||
effectiveMaximumValue =
|
||||
effectiveMinimumValue + MINIMUM_POSITIVE_RANGE_PADDING;
|
||||
effectiveMinimumValue +
|
||||
COMMON_CHART_CONSTANTS.MINIMUM_POSITIVE_RANGE_PADDING;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
|
||||
export const computeMaxLabelLengthForMargin = ({
|
||||
marginSize,
|
||||
axisFontSize,
|
||||
}: {
|
||||
marginSize: number;
|
||||
axisFontSize: number;
|
||||
}): number => {
|
||||
const characterWidth =
|
||||
axisFontSize *
|
||||
COMMON_CHART_CONSTANTS.HORIZONTAL_LABEL_CHARACTER_WIDTH_RATIO;
|
||||
const availableWidth =
|
||||
marginSize - COMMON_CHART_CONSTANTS.TICK_PADDING_ALLOWANCE;
|
||||
const calculatedLength = Math.max(
|
||||
COMMON_CHART_CONSTANTS.TICK_MINIMUM_NUMBER_OF_DISPLAYED_CHARACTERS,
|
||||
Math.floor(availableWidth / characterWidth),
|
||||
);
|
||||
|
||||
return calculatedLength;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const ELLIPSIS_LENGTH = 3;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity';
|
||||
import { formatDateByGranularity } from '@/page-layout/widgets/graph/utils/formatDateByGranularity';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity';
|
||||
import { GRAPH_DEFAULT_ORDER_BY } from '@/page-layout/widgets/graph/constants/GraphDefaultOrderBy';
|
||||
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
|
||||
import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind';
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity';
|
||||
import { getGroupByOrderBy } from '@/page-layout/widgets/graph/utils/getGroupByOrderBy';
|
||||
import { isRelationNestedFieldDateKind } from '@/page-layout/widgets/graph/utils/isRelationNestedFieldDateKind';
|
||||
import {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
|
||||
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity';
|
||||
import { getRelationFieldOrderBy } from '@/page-layout/widgets/graph/utils/getRelationFieldOrderBy';
|
||||
import {
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity.constant';
|
||||
import { GRAPH_DEFAULT_DATE_GRANULARITY } from '@/page-layout/widgets/graph/constants/GraphDefaultDateGranularity';
|
||||
import {
|
||||
type ObjectRecordGroupByDateGranularity,
|
||||
type ObjectRecordOrderByForRelationField,
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
|
||||
const TICK_ROTATION_ANGLE_RAD =
|
||||
(Math.abs(COMMON_CHART_CONSTANTS.TICK_ROTATION_ANGLE) * Math.PI) / 180;
|
||||
const MIN_CALCULATED_LENGTH = 1;
|
||||
const TICK_MARGIN = 1;
|
||||
|
||||
export type TickRotationConfig = {
|
||||
tickRotation: number;
|
||||
maxLabelLength: number;
|
||||
};
|
||||
|
||||
export const getTickRotationConfig = ({
|
||||
widthPerTick,
|
||||
axisFontSize,
|
||||
}: {
|
||||
widthPerTick: number;
|
||||
axisFontSize: number;
|
||||
}): TickRotationConfig => {
|
||||
const shouldRotate =
|
||||
widthPerTick <
|
||||
COMMON_CHART_CONSTANTS.TICK_MINIMUM_NUMBER_OF_DISPLAYED_CHARACTERS *
|
||||
COMMON_CHART_CONSTANTS.HORIZONTAL_LABEL_CHARACTER_WIDTH_RATIO *
|
||||
axisFontSize +
|
||||
TICK_MARGIN;
|
||||
|
||||
if (shouldRotate) {
|
||||
const characterWidth =
|
||||
axisFontSize * COMMON_CHART_CONSTANTS.ROTATED_LABEL_CHARACTER_WIDTH_RATIO;
|
||||
const calculatedLength = Math.max(
|
||||
MIN_CALCULATED_LENGTH,
|
||||
Math.floor(
|
||||
COMMON_CHART_CONSTANTS.MARGIN_BOTTOM_WITHOUT_LABEL /
|
||||
(characterWidth * Math.sin(TICK_ROTATION_ANGLE_RAD)),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
tickRotation: COMMON_CHART_CONSTANTS.TICK_ROTATION_ANGLE,
|
||||
maxLabelLength: calculatedLength,
|
||||
};
|
||||
}
|
||||
|
||||
const characterWidth =
|
||||
axisFontSize *
|
||||
COMMON_CHART_CONSTANTS.HORIZONTAL_LABEL_CHARACTER_WIDTH_RATIO;
|
||||
|
||||
const calculatedLength = Math.max(
|
||||
MIN_CALCULATED_LENGTH,
|
||||
Math.floor(widthPerTick / characterWidth),
|
||||
);
|
||||
|
||||
return {
|
||||
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
maxLabelLength: calculatedLength,
|
||||
};
|
||||
};
|
||||
+3
-3
@@ -2,7 +2,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { GRAPH_DEFAULT_COLOR } from '@/page-layout/widgets/graph/constants/GraphDefaultColor.constant';
|
||||
import { LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfDataPoints.constant';
|
||||
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
@@ -44,7 +44,7 @@ export const transformOneDimensionalGroupByToLineChartData = ({
|
||||
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
|
||||
const limitedResults = rawResults.slice(
|
||||
0,
|
||||
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
);
|
||||
|
||||
const formattedValues = formatPrimaryDimensionValues({
|
||||
@@ -112,7 +112,7 @@ export const transformOneDimensionalGroupByToLineChartData = ({
|
||||
return {
|
||||
series,
|
||||
hasTooManyGroups:
|
||||
rawResults.length > LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
rawResults.length > LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS,
|
||||
formattedToRawLookup,
|
||||
};
|
||||
};
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
|
||||
import { LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfDataPoints.constant';
|
||||
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
|
||||
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
|
||||
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
|
||||
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
|
||||
@@ -73,7 +73,10 @@ export const transformTwoDimensionalGroupByToLineChartData = ({
|
||||
// TODO: Add a limit to the query instead of checking here (issue: twentyhq/core-team-issues#1600)
|
||||
const isNewX = !xValueSet.has(xValue);
|
||||
|
||||
if (isNewX && xValueSet.size >= LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS) {
|
||||
if (
|
||||
isNewX &&
|
||||
xValueSet.size >= LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS
|
||||
) {
|
||||
hasTooManyGroups = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user