[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:
Raphaël Bosi
2025-12-16 17:39:42 +01:00
committed by GitHub
parent 5a5bf112cf
commit 576019e465
97 changed files with 722 additions and 322 deletions
@@ -1,9 +1,9 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio';
import { type AggregateChartOperation } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/types/AggregateChartOperation';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
@@ -6,7 +6,7 @@ import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useW
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { DateAggregateOperations } from '@/object-record/record-table/constants/DateAggregateOperations';
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio';
import { type AggregateChartOperation } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/types/AggregateChartOperation';
import { getAggregateChartOperationLabel } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/utils/getAggregateChartOperationLabel';
import { getAvailableAggregateOperationsForAggregateChart } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/utils/getAvailableAggregateOperationsForAggregateChart';
@@ -1,6 +1,6 @@
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
@@ -1,6 +1,6 @@
import { getDateGranularityPluralLabel } from '@/command-menu/pages/page-layout/utils/getDateGranularityPluralLabel';
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfDataPoints.constant';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant';
import { t } from '@lingui/core/macro';
import { type ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
@@ -24,10 +24,10 @@ export const getChartLimitMessage = ({
}: GetChartLimitMessageParams): string => {
const maxItems =
graphType === GraphType.LINE
? LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS
? LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS
: graphType === GraphType.VERTICAL_BAR ||
graphType === GraphType.HORIZONTAL_BAR
? BAR_CHART_MAXIMUM_NUMBER_OF_BARS
? BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS
: PIE_CHART_MAXIMUM_NUMBER_OF_SLICES;
if (isPrimaryAxisDate && isDefined(primaryAxisDateGranularity)) {
@@ -1,9 +1,9 @@
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { GraphWidgetLegendDot } from '@/page-layout/widgets/graph/components/GraphWidgetLegendDot';
import { LEGEND_HIGHLIGHT_DIMMED_OPACITY } from '@/page-layout/widgets/graph/constants/LegendHighlightDimmedOpacity.constant';
import { LEGEND_ITEM_ESTIMATED_WIDTH } from '@/page-layout/widgets/graph/constants/LegendItemEstimatedWidth.constant';
import { LEGEND_LABEL_MAX_WIDTH } from '@/page-layout/widgets/graph/constants/LegendLabelMaxWidth.constant';
import { LEGEND_PAGINATION_CONTROLS_WIDTH } from '@/page-layout/widgets/graph/constants/LegendPaginationControlsWidth.constant';
import { LEGEND_ITEM_ESTIMATED_WIDTH } from '@/page-layout/widgets/graph/constants/LegendItemEstimatedWidth';
import { LEGEND_LABEL_MAX_WIDTH } from '@/page-layout/widgets/graph/constants/LegendLabelMaxWidth';
import { LEGEND_PAGINATION_CONTROLS_WIDTH } from '@/page-layout/widgets/graph/constants/LegendPaginationControlsWidth';
import { useLegendItemToggle } from '@/page-layout/widgets/graph/hooks/useLegendItemToggle';
import { graphWidgetHiddenLegendIdsComponentState } from '@/page-layout/widgets/graph/states/graphWidgetHiddenLegendIdsComponentState';
import { graphWidgetHighlightedLegendIdComponentState } from '@/page-layout/widgets/graph/states/graphWidgetHighlightedLegendIdComponentState';
@@ -0,0 +1,24 @@
export const COMMON_CHART_CONSTANTS = {
MINIMUM_WIDTH_PER_TICK_ROTATED: 20,
ROTATED_LABELS_EXTRA_BOTTOM_MARGIN: 20,
MARGIN_TOP: 20,
MARGIN_RIGHT: 20,
MARGIN_LEFT_WITHOUT_LABEL: 65,
MARGIN_LEFT_WITH_LABEL: 80,
MARGIN_BOTTOM_WITHOUT_LABEL: 50,
MARGIN_BOTTOM_WITH_LABEL: 65,
TICK_PADDING: 5,
TICK_SIZE: 0,
AXIS_FONT_SIZE: 11,
LEFT_AXIS_LEGEND_OFFSET_PADDING: 5,
BOTTOM_AXIS_LEGEND_OFFSET: 55,
TICK_MINIMUM_NUMBER_OF_DISPLAYED_CHARACTERS: 10,
HORIZONTAL_LABEL_CHARACTER_WIDTH_RATIO: 0.6,
ROTATED_LABEL_CHARACTER_WIDTH_RATIO: 0.5,
TICK_ROTATION_ANGLE: -45,
TICK_PADDING_ALLOWANCE: 10,
MIN_TICK_LABEL_LENGTH: 5,
POSITIVE_RANGE_PADDING_RATIO: 0.1,
MINIMUM_POSITIVE_RANGE_PADDING: 1,
NO_ROTATION_ANGLE: 0,
} as const;
@@ -0,0 +1 @@
export const ROTATION_THRESHOLD_WIDTH = 50;
@@ -1,5 +1,5 @@
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { type DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { type DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio';
export type AggregateChartOperation =
| ExtendedAggregateOperations
@@ -1,4 +1,4 @@
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { getAvailableAggregateOperationsForAggregateChart } from '../getAvailableAggregateOperationsForAggregateChart';
@@ -1,5 +1,5 @@
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio';
import { type AggregateChartOperation } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/types/AggregateChartOperation';
import { t } from '@lingui/core/macro';
@@ -1,5 +1,5 @@
import { getAvailableAggregateOperationsForFieldMetadataType } from '@/object-record/record-table/record-table-footer/utils/getAvailableAggregateOperationsForFieldMetadataType';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio.constant';
import { DASHBOARD_AGGREGATE_OPERATION_RATIO } from '@/page-layout/widgets/graph/constants/DashboardAggregateOperationRatio';
import { type AggregateChartOperation } from '@/page-layout/widgets/graph/graphWidgetAggregateChart/types/AggregateChartOperation';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -1,6 +1,5 @@
import { LEGEND_HIGHLIGHT_DIMMED_OPACITY } from '@/page-layout/widgets/graph/constants/LegendHighlightDimmedOpacity.constant';
import { BAR_CHART_HOVER_BRIGHTNESS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartHoverBrightness';
import { BAR_CHART_MAXIMUM_WIDTH } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/MaximumBarWidth';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { graphWidgetHighlightedLegendIdComponentState } from '@/page-layout/widgets/graph/states/graphWidgetHighlightedLegendIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
@@ -33,7 +32,9 @@ const StyledBarRect = styled(animated.rect)<{
&:hover {
filter: ${({ $isInteractive }) =>
$isInteractive ? `brightness(${BAR_CHART_HOVER_BRIGHTNESS})` : 'none'};
$isInteractive
? `brightness(${BAR_CHART_CONSTANTS.HOVER_BRIGHTNESS})`
: 'none'};
}
`;
@@ -152,12 +153,12 @@ export const CustomBarItem = <D extends BarDatum>({
const constrainedThicknessDimension = to(
unconstrainedThicknessDimension,
(dimension) => Math.min(dimension, BAR_CHART_MAXIMUM_WIDTH),
(dimension) => Math.min(dimension, BAR_CHART_CONSTANTS.MAXIMUM_WIDTH),
);
const centeringOffset = to(unconstrainedThicknessDimension, (dimension) =>
dimension > BAR_CHART_MAXIMUM_WIDTH
? (dimension - BAR_CHART_MAXIMUM_WIDTH) / 2
dimension > BAR_CHART_CONSTANTS.MAXIMUM_WIDTH
? (dimension - BAR_CHART_CONSTANTS.MAXIMUM_WIDTH) / 2
: 0,
);
@@ -1,5 +1,5 @@
import { GraphWidgetFloatingTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip';
import { BAR_CHART_TOOLTIP_OFFSET_PX } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartTooltipOffsetPx';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { graphWidgetBarTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetBarChart/states/graphWidgetBarTooltipComponentState';
import { type BarChartEnrichedKey } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartEnrichedKey';
import { getBarChartTooltipData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTooltipData';
@@ -63,7 +63,7 @@ export const GraphBarChartTooltip = ({
<GraphWidgetFloatingTooltip
reference={reference}
boundary={containerElement}
tooltipOffsetFromAnchorInPx={BAR_CHART_TOOLTIP_OFFSET_PX}
tooltipOffsetFromAnchorInPx={BAR_CHART_CONSTANTS.TOOLTIP_OFFSET_PX}
items={tooltipData?.tooltipItems ?? []}
indexLabel={tooltipData?.indexLabel}
highlightedKey={tooltipData?.hoveredKey}
@@ -4,7 +4,7 @@ import { NoDataLayer } from '@/page-layout/widgets/graph/components/NoDataLayer'
import { CustomBarItem } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/CustomBarItem';
import { CustomTotalsLayer } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/CustomTotalsLayer';
import { GraphBarChartTooltip } from '@/page-layout/widgets/graph/graphWidgetBarChart/components/GraphBarChartTooltip';
import { BAR_CHART_OUTER_PADDING_RATIO } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartOuterPaddingRatio';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { useBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartData';
import { useBarChartTheme } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useBarChartTheme';
import { graphWidgetBarTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetBarChart/states/graphWidgetBarTooltipComponentState';
@@ -15,7 +15,6 @@ import { calculateValueRangeFromBarChartKeys } from '@/page-layout/widgets/graph
import { getBarChartAxisConfigs } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs';
import { getBarChartColor } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartColor';
import { getBarChartInnerPadding } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartInnerPadding';
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
import { getBarChartTickConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig';
import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange';
import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues';
@@ -185,20 +184,18 @@ export const GraphWidgetBarChart = ({
debouncedHideTooltip();
}, [debouncedHideTooltip]);
const { axisBottom: axisBottomConfig, axisLeft: axisLeftConfig } =
getBarChartAxisConfigs({
width: chartWidth,
height: chartHeight,
data,
layout,
indexBy,
xAxisLabel,
yAxisLabel,
formatOptions,
axisFontSize: chartTheme.axis.ticks.text.fontSize,
valueTickValues,
tickConfig,
});
const {
axisBottom: axisBottomConfig,
axisLeft: axisLeftConfig,
margins,
} = getBarChartAxisConfigs({
layout,
xAxisLabel,
yAxisLabel,
formatOptions,
valueTickValues,
tickConfig,
});
const BarItemWithContext = useMemo(
() => (props: BarItemProps<BarDatum>) => (
@@ -260,8 +257,6 @@ export const GraphWidgetBarChart = ({
]
: undefined;
const margins = getBarChartMargins({ xAxisLabel, yAxisLabel, layout });
return (
<StyledContainer id={id}>
<GraphWidgetChartContainer
@@ -282,7 +277,7 @@ export const GraphWidgetBarChart = ({
keys={visibleKeys}
indexBy={indexBy}
margin={margins}
padding={BAR_CHART_OUTER_PADDING_RATIO}
padding={BAR_CHART_CONSTANTS.OUTER_PADDING_RATIO}
groupMode={groupMode}
layout={layout}
valueScale={{
@@ -0,0 +1,25 @@
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
export const BAR_CHART_CONSTANTS = {
...COMMON_CHART_CONSTANTS,
MAXIMUM_NUMBER_OF_BARS: 100,
MAXIMUM_NUMBER_OF_GROUPS_PER_BAR: 50,
MAXIMUM_WIDTH: 32,
OUTER_PADDING_RATIO: 0.05,
DEFAULT_INNER_PADDING: 4,
MAXIMUM_VALUE_TICK_COUNT: 6,
MINIMUM_VALUE_TICK_COUNT: 2,
MINIMUM_WIDTH_PER_TICK: 100,
MIN_TICK_SPACING_HEIGHT_RATIO: 2.5,
TOOLTIP_OFFSET_PX: 2,
TOOLTIP_SCROLLABLE_ITEM_THRESHOLD: 5,
HOVER_BRIGHTNESS: 0.85,
MINIMUM_BAR_WIDTH: 2,
DATE_GRANULARITIES_WITHOUT_GAP_FILLING: new Set([
ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK,
ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR,
ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR,
ObjectRecordGroupByDateGranularity.NONE,
]),
} as const;
@@ -1,8 +0,0 @@
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
export const BAR_CHART_DATE_GRANULARITIES_WITHOUT_GAP_FILLING = new Set([
ObjectRecordGroupByDateGranularity.DAY_OF_THE_WEEK,
ObjectRecordGroupByDateGranularity.MONTH_OF_THE_YEAR,
ObjectRecordGroupByDateGranularity.QUARTER_OF_THE_YEAR,
ObjectRecordGroupByDateGranularity.NONE,
]);
@@ -1 +0,0 @@
export const BAR_CHART_DEFAULT_INNER_PADDING = 4;
@@ -1 +0,0 @@
export const BAR_CHART_HOVER_BRIGHTNESS = 0.85;
@@ -1 +0,0 @@
export const BAR_CHART_MAXIMUM_NUMBER_OF_BARS = 100;
@@ -1 +0,0 @@
export const BAR_CHART_MAXIMUM_NUMBER_OF_GROUPS_PER_BAR = 50;
@@ -1 +0,0 @@
export const BAR_CHART_MAXIMUM_VALUE_TICK_COUNT = 6;
@@ -1 +0,0 @@
export const BAR_CHART_MIN_TICK_SPACING_HEIGHT_RATIO = 2.5;
@@ -1 +0,0 @@
export const BAR_CHART_MINIMUM_VALUE_TICK_COUNT = 2;
@@ -1 +0,0 @@
export const BAR_CHART_MINIMUM_WIDTH_PER_TICK = 100;
@@ -1 +0,0 @@
export const BAR_CHART_OUTER_PADDING_RATIO = 0.05;
@@ -1 +0,0 @@
export const BAR_CHART_TOOLTIP_OFFSET_PX = 2;
@@ -1 +0,0 @@
export const BAR_CHART_TOOLTIP_SCROLLABLE_ITEM_THRESHOLD = 5;
@@ -1 +0,0 @@
export const BAR_CHART_MAXIMUM_WIDTH = 32;
@@ -1,4 +1,4 @@
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { generateDateGroupsInRange } from '../generateDateGroupsInRange';
@@ -63,9 +63,11 @@ describe('generateDateGroupsInRange', () => {
});
expect(result.dates.length).toBeLessThanOrEqual(
BAR_CHART_MAXIMUM_NUMBER_OF_BARS,
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS,
);
expect(result.dates.length).toBe(
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS,
);
expect(result.dates.length).toBe(BAR_CHART_MAXIMUM_NUMBER_OF_BARS);
expect(result.wasTruncated).toBe(true);
});
});
@@ -1,6 +1,5 @@
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups.constant';
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { BAR_CHART_MAXIMUM_NUMBER_OF_GROUPS_PER_BAR } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfGroupsPerBar.constant';
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import {
type BarChartConfiguration,
BarChartGroupMode,
@@ -16,7 +15,8 @@ describe('getBarChartQueryLimit', () => {
} as BarChartConfiguration);
expect(result).toBe(
BAR_CHART_MAXIMUM_NUMBER_OF_BARS + EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
);
});
@@ -28,8 +28,8 @@ describe('getBarChartQueryLimit', () => {
} as BarChartConfiguration);
expect(result).toBe(
BAR_CHART_MAXIMUM_NUMBER_OF_BARS *
BAR_CHART_MAXIMUM_NUMBER_OF_GROUPS_PER_BAR +
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS *
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_GROUPS_PER_BAR +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
);
});
@@ -42,7 +42,8 @@ describe('getBarChartQueryLimit', () => {
} as BarChartConfiguration);
expect(result).toBe(
BAR_CHART_MAXIMUM_NUMBER_OF_BARS + EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
);
});
});
@@ -0,0 +1,40 @@
import { truncateTickLabel } from '../truncateTickLabel';
describe('truncateTickLabel', () => {
it('should return the original string when it fits within maxLength', () => {
expect(truncateTickLabel('Hello', 10)).toBe('Hello');
expect(truncateTickLabel('Test', 4)).toBe('Test');
});
it('should truncate and add ellipsis when string exceeds maxLength', () => {
expect(truncateTickLabel('Hello World', 8)).toBe('Hello...');
expect(truncateTickLabel('LongLabel', 6)).toBe('Lon...');
});
it('should handle numeric values', () => {
expect(truncateTickLabel(12345, 4)).toBe('1...');
expect(truncateTickLabel(12345, 10)).toBe('12345');
});
it('should return partial ellipsis when maxLength is 3 or less', () => {
expect(truncateTickLabel('Hello', 3)).toBe('...');
expect(truncateTickLabel('Hello', 2)).toBe('..');
expect(truncateTickLabel('Hello', 1)).toBe('.');
});
it('should handle maxLength of 0', () => {
expect(truncateTickLabel('Hello', 0)).toBe('');
});
it('should handle empty string', () => {
expect(truncateTickLabel('', 5)).toBe('');
});
it('should handle maxLength equal to string length', () => {
expect(truncateTickLabel('Hello', 5)).toBe('Hello');
});
it('should handle maxLength of 4 correctly', () => {
expect(truncateTickLabel('Hello', 4)).toBe('H...');
});
});
@@ -1,5 +1,4 @@
const AVERAGE_CHARACTER_WIDTH_RATIO = 0.6;
const MIN_TICK_LABEL_LENGTH = 5;
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
export const calculateMaxTickLabelLength = ({
widthPerTick,
@@ -8,8 +7,13 @@ export const calculateMaxTickLabelLength = ({
widthPerTick: number;
axisFontSize: number;
}): number => {
const averageCharacterWidth = axisFontSize * AVERAGE_CHARACTER_WIDTH_RATIO;
const averageCharacterWidth =
axisFontSize *
COMMON_CHART_CONSTANTS.HORIZONTAL_LABEL_CHARACTER_WIDTH_RATIO;
const calculatedLength = Math.floor(widthPerTick / averageCharacterWidth);
return Math.max(MIN_TICK_LABEL_LENGTH, calculatedLength);
return Math.max(
COMMON_CHART_CONSTANTS.MIN_TICK_LABEL_LENGTH,
calculatedLength,
);
};
@@ -1,4 +1,4 @@
import { BAR_CHART_MINIMUM_WIDTH_PER_TICK } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumWidthPerTick';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { computeMinHeightPerTick } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeMinHeightPerTick';
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
@@ -37,14 +37,20 @@ export const computeBarChartCategoryTickValues = ({
const availableAxisSize = axisSize - totalMargins;
const minimumSizePerTick =
layout === BarChartLayout.VERTICAL
? BAR_CHART_MINIMUM_WIDTH_PER_TICK
: computeMinHeightPerTick({ axisFontSize });
if (layout === BarChartLayout.HORIZONTAL) {
return computeChartCategoryTickValues({
availableSize: availableAxisSize,
minimumSizePerTick: computeMinHeightPerTick({ axisFontSize }),
values,
});
}
const widthPerTick = data.length > 0 ? availableAxisSize / data.length : 0;
return computeChartCategoryTickValues({
availableSize: availableAxisSize,
minimumSizePerTick,
minimumSizePerTick: BAR_CHART_CONSTANTS.MINIMUM_WIDTH_PER_TICK_ROTATED,
values,
widthPerTick,
});
};
@@ -1,4 +1,4 @@
import { BAR_CHART_MINIMUM_WIDTH_PER_TICK } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumWidthPerTick';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { computeMinHeightPerTick } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeMinHeightPerTick';
@@ -16,7 +16,7 @@ export const computeBarChartValueTickCount = ({
const minTickSize =
layout === BarChartLayout.VERTICAL
? computeMinHeightPerTick({ axisFontSize })
: BAR_CHART_MINIMUM_WIDTH_PER_TICK;
: BAR_CHART_CONSTANTS.MINIMUM_WIDTH_PER_TICK;
return Math.max(1, Math.floor(axisSize / minTickSize));
};
@@ -1,9 +1,9 @@
import { BAR_CHART_MIN_TICK_SPACING_HEIGHT_RATIO } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinTickSpacingHeightRatio';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
export const computeMinHeightPerTick = ({
axisFontSize,
}: {
axisFontSize: number;
}): number => {
return axisFontSize * BAR_CHART_MIN_TICK_SPACING_HEIGHT_RATIO;
return axisFontSize * BAR_CHART_CONSTANTS.MIN_TICK_SPACING_HEIGHT_RATIO;
};
@@ -1,4 +1,4 @@
import { BAR_CHART_DATE_GRANULARITIES_WITHOUT_GAP_FILLING } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartDateGranularitiesWithoutGapFilling.constant';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { fillDateGapsInOneDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInOneDimensionalBarChartData';
import { fillDateGapsInTwoDimensionalBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInTwoDimensionalBarChartData';
import { type SupportedDateGranularity } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getDateGroupsFromData';
@@ -25,7 +25,11 @@ export const fillDateGapsInBarChartData = ({
return { data, wasTruncated: false };
}
if (BAR_CHART_DATE_GRANULARITIES_WITHOUT_GAP_FILLING.has(dateGranularity)) {
if (
BAR_CHART_CONSTANTS.DATE_GRANULARITIES_WITHOUT_GAP_FILLING.has(
dateGranularity,
)
) {
return { data, wasTruncated: false };
}
@@ -1,4 +1,4 @@
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
@@ -31,7 +31,7 @@ export const generateDateGroupsInRange = ({
let currentDateCursor = new Date(startDate);
while (currentDateCursor <= endDate) {
if (iterations >= BAR_CHART_MAXIMUM_NUMBER_OF_BARS) {
if (iterations >= BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS) {
wasTruncated = true;
break;
}
@@ -1,51 +1,29 @@
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
import {
type BarChartTickConfig,
getBarChartTickConfig,
} from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig';
import { truncateTickLabel } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel';
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
import { type BarDatum } from '@nivo/bar';
const LEFT_AXIS_LEGEND_OFFSET_PADDING = 5;
const TICK_PADDING = 5;
const BOTTOM_AXIS_LEGEND_OFFSET = 40;
const COMMON_AXIS_CONFIG = {
tickSize: 0,
tickPadding: TICK_PADDING,
tickRotation: 0,
legendPosition: 'middle' as const,
};
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { type BarChartTickConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig';
type GetBarChartAxisConfigsProps = {
width: number;
height: number;
data: BarDatum[];
layout: BarChartLayout;
indexBy: string;
xAxisLabel?: string;
yAxisLabel?: string;
formatOptions?: GraphValueFormatOptions;
axisFontSize?: number;
valueTickValues?: number[];
tickConfig?: BarChartTickConfig;
tickConfig: BarChartTickConfig;
};
export const getBarChartAxisConfigs = ({
width,
height,
data,
layout,
indexBy,
xAxisLabel,
yAxisLabel,
formatOptions,
axisFontSize = 11,
valueTickValues,
tickConfig,
}: GetBarChartAxisConfigsProps) => {
@@ -54,55 +32,72 @@ export const getBarChartAxisConfigs = ({
numberOfValueTicks,
maxBottomAxisTickLabelLength,
maxLeftAxisTickLabelLength,
} =
tickConfig ??
getBarChartTickConfig({
width,
height,
data,
indexBy,
xAxisLabel,
yAxisLabel,
axisFontSize,
layout,
});
bottomAxisTickRotation,
} = tickConfig;
const margins = getBarChartMargins({ xAxisLabel, yAxisLabel, layout });
const resolvedValueTickValues =
valueTickValues && valueTickValues.length > 0
? valueTickValues
: numberOfValueTicks;
const baseMargins = getBarChartMargins({ xAxisLabel, yAxisLabel, layout });
const hasRotation = bottomAxisTickRotation !== 0;
const margins =
layout === BarChartLayout.VERTICAL && hasRotation
? {
...baseMargins,
bottom:
baseMargins.bottom +
COMMON_CHART_CONSTANTS.ROTATED_LABELS_EXTRA_BOTTOM_MARGIN,
}
: baseMargins;
if (layout === BarChartLayout.VERTICAL) {
return {
axisBottom: {
...COMMON_AXIS_CONFIG,
tickSize: BAR_CHART_CONSTANTS.TICK_SIZE,
tickPadding: BAR_CHART_CONSTANTS.TICK_PADDING,
legendPosition: 'middle' as const,
tickValues: categoryTickValues,
tickRotation: bottomAxisTickRotation,
legend: xAxisLabel,
legendOffset: BOTTOM_AXIS_LEGEND_OFFSET,
legendOffset:
BAR_CHART_CONSTANTS.BOTTOM_AXIS_LEGEND_OFFSET +
(hasRotation
? BAR_CHART_CONSTANTS.ROTATED_LABELS_EXTRA_BOTTOM_MARGIN
: 0),
format: (value: string | number) =>
truncateTickLabel(String(value), maxBottomAxisTickLabelLength),
},
axisLeft: {
...COMMON_AXIS_CONFIG,
tickSize: BAR_CHART_CONSTANTS.TICK_SIZE,
tickPadding: BAR_CHART_CONSTANTS.TICK_PADDING,
legendPosition: 'middle' as const,
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
tickValues: resolvedValueTickValues,
legend: yAxisLabel,
legendOffset: -margins.left + LEFT_AXIS_LEGEND_OFFSET_PADDING,
legendOffset:
-margins.left + BAR_CHART_CONSTANTS.LEFT_AXIS_LEGEND_OFFSET_PADDING,
format: (value: number) =>
truncateTickLabel(
formatGraphValue(value, formatOptions ?? {}),
maxLeftAxisTickLabelLength,
),
},
margins,
};
}
return {
axisBottom: {
...COMMON_AXIS_CONFIG,
tickSize: BAR_CHART_CONSTANTS.TICK_SIZE,
tickPadding: BAR_CHART_CONSTANTS.TICK_PADDING,
legendPosition: 'middle' as const,
tickRotation: BAR_CHART_CONSTANTS.NO_ROTATION_ANGLE,
tickValues: resolvedValueTickValues,
legend: yAxisLabel,
legendOffset: BOTTOM_AXIS_LEGEND_OFFSET,
legendOffset: BAR_CHART_CONSTANTS.BOTTOM_AXIS_LEGEND_OFFSET,
format: (value: number) =>
truncateTickLabel(
formatGraphValue(value, formatOptions ?? {}),
@@ -110,12 +105,17 @@ export const getBarChartAxisConfigs = ({
),
},
axisLeft: {
...COMMON_AXIS_CONFIG,
tickSize: COMMON_CHART_CONSTANTS.TICK_SIZE,
tickPadding: COMMON_CHART_CONSTANTS.TICK_PADDING,
legendPosition: 'middle' as const,
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
tickValues: categoryTickValues,
legend: xAxisLabel,
legendOffset: -margins.left + LEFT_AXIS_LEGEND_OFFSET_PADDING,
legendOffset:
-margins.left + BAR_CHART_CONSTANTS.LEFT_AXIS_LEGEND_OFFSET_PADDING,
format: (value: string | number) =>
truncateTickLabel(String(value), maxLeftAxisTickLabelLength),
},
margins,
};
};
@@ -1,5 +1,4 @@
import { BAR_CHART_DEFAULT_INNER_PADDING } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartDefaultInnerPadding';
import { BAR_CHART_OUTER_PADDING_RATIO } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartOuterPaddingRatio';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
type BarChartMargins = {
@@ -19,8 +18,6 @@ type GetBarChartInnerPaddingProps = {
groupMode?: 'grouped' | 'stacked';
};
const MINIMUM_BAR_WIDTH = 2;
export const getBarChartInnerPadding = ({
chartWidth,
chartHeight,
@@ -35,7 +32,7 @@ export const getBarChartInnerPadding = ({
}
if (dataLength === 0 || keysLength === 0) {
return BAR_CHART_DEFAULT_INNER_PADDING;
return BAR_CHART_CONSTANTS.DEFAULT_INNER_PADDING;
}
const availableSpace =
@@ -44,13 +41,21 @@ export const getBarChartInnerPadding = ({
: chartHeight - margins.top - margins.bottom;
const spacePerGroup =
(availableSpace / dataLength) * (1 - BAR_CHART_OUTER_PADDING_RATIO);
(availableSpace / dataLength) *
(1 - BAR_CHART_CONSTANTS.OUTER_PADDING_RATIO);
const spacePerBar = spacePerGroup / keysLength;
if (spacePerBar < MINIMUM_BAR_WIDTH + BAR_CHART_DEFAULT_INNER_PADDING) {
return Math.max(0, (spacePerBar - MINIMUM_BAR_WIDTH) / 2);
if (
spacePerBar <
BAR_CHART_CONSTANTS.MINIMUM_BAR_WIDTH +
BAR_CHART_CONSTANTS.DEFAULT_INNER_PADDING
) {
return Math.max(
0,
(spacePerBar - BAR_CHART_CONSTANTS.MINIMUM_BAR_WIDTH) / 2,
);
}
return BAR_CHART_DEFAULT_INNER_PADDING;
return BAR_CHART_CONSTANTS.DEFAULT_INNER_PADDING;
};
@@ -1,32 +1,33 @@
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { isDefined } from 'twenty-shared/utils';
const BAR_CHART_MARGINS = {
top: 20,
right: 20,
bottom: 45,
left: 65,
top: COMMON_CHART_CONSTANTS.MARGIN_TOP,
right: COMMON_CHART_CONSTANTS.MARGIN_RIGHT,
bottom: COMMON_CHART_CONSTANTS.MARGIN_BOTTOM_WITHOUT_LABEL,
left: COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITHOUT_LABEL,
} as const;
const BAR_CHART_MARGINS_WITH_BOTH_LABELS = {
top: 20,
right: 20,
bottom: 60,
left: 80,
top: COMMON_CHART_CONSTANTS.MARGIN_TOP,
right: COMMON_CHART_CONSTANTS.MARGIN_RIGHT,
bottom: COMMON_CHART_CONSTANTS.MARGIN_BOTTOM_WITH_LABEL,
left: COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITH_LABEL,
} as const;
const BAR_CHART_MARGINS_WITH_X_LABEL = {
top: 20,
right: 20,
bottom: 60,
left: 65,
top: COMMON_CHART_CONSTANTS.MARGIN_TOP,
right: COMMON_CHART_CONSTANTS.MARGIN_RIGHT,
bottom: COMMON_CHART_CONSTANTS.MARGIN_BOTTOM_WITH_LABEL,
left: COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITHOUT_LABEL,
} as const;
const BAR_CHART_MARGINS_WITH_Y_LABEL = {
top: 20,
right: 20,
bottom: 45,
left: 80,
top: COMMON_CHART_CONSTANTS.MARGIN_TOP,
right: COMMON_CHART_CONSTANTS.MARGIN_RIGHT,
bottom: COMMON_CHART_CONSTANTS.MARGIN_BOTTOM_WITHOUT_LABEL,
left: COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITH_LABEL,
} as const;
export const getBarChartMargins = ({
@@ -1,6 +1,5 @@
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups.constant';
import { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { BAR_CHART_MAXIMUM_NUMBER_OF_GROUPS_PER_BAR } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfGroupsPerBar.constant';
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { isChartConfigurationTwoDimensional } from '@/page-layout/widgets/graph/utils/isChartConfigurationTwoDimensional';
import {
BarChartGroupMode,
@@ -15,13 +14,14 @@ export const getBarChartQueryLimit = (
configuration.groupMode === BarChartGroupMode.STACKED
) {
return (
BAR_CHART_MAXIMUM_NUMBER_OF_BARS *
BAR_CHART_MAXIMUM_NUMBER_OF_GROUPS_PER_BAR +
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS *
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_GROUPS_PER_BAR +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS
);
}
return (
BAR_CHART_MAXIMUM_NUMBER_OF_BARS + EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS
);
};
@@ -1,20 +1,19 @@
import { BAR_CHART_MAXIMUM_VALUE_TICK_COUNT } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumValueTickCount';
import { BAR_CHART_MINIMUM_VALUE_TICK_COUNT } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMinimumValueTickCount';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { calculateMaxTickLabelLength } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateMaxTickLabelLength';
import { calculateWidthPerTick } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateWidthPerTick';
import { computeBarChartCategoryTickValues } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartCategoryTickValues';
import { computeBarChartValueTickCount } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeBarChartValueTickCount';
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
import { computeMaxLabelLengthForMargin } from '@/page-layout/widgets/graph/utils/computeMaxLabelLengthForMargin';
import { getTickRotationConfig } from '@/page-layout/widgets/graph/utils/getTickRotationConfig';
import { type BarDatum } from '@nivo/bar';
const MAX_LEFT_AXIS_LABEL_LENGTH = 10;
export type BarChartTickConfig = {
categoryTickValues: (string | number)[];
numberOfValueTicks: number;
maxBottomAxisTickLabelLength: number;
maxLeftAxisTickLabelLength: number;
bottomAxisTickRotation: number;
};
export const getBarChartTickConfig = ({
@@ -38,8 +37,8 @@ export const getBarChartTickConfig = ({
}): BarChartTickConfig => {
const clampValueTickCount = (tickCount: number) =>
Math.min(
BAR_CHART_MAXIMUM_VALUE_TICK_COUNT,
Math.max(BAR_CHART_MINIMUM_VALUE_TICK_COUNT, tickCount),
BAR_CHART_CONSTANTS.MAXIMUM_VALUE_TICK_COUNT,
Math.max(BAR_CHART_CONSTANTS.MINIMUM_VALUE_TICK_COUNT, tickCount),
);
const categoryTickValues = computeBarChartCategoryTickValues({
@@ -73,18 +72,29 @@ export const getBarChartTickConfig = ({
valueTickCount: numberOfValueTicks,
});
const maxBottomAxisTickLabelLength = calculateMaxTickLabelLength({
widthPerTick,
const actualDataPointCount = data.length;
const widthPerDataPoint =
layout === BarChartLayout.VERTICAL &&
actualDataPointCount > 0 &&
availableWidth > 0
? availableWidth / actualDataPointCount
: widthPerTick;
const tickRotationConfig = getTickRotationConfig({
widthPerTick: widthPerDataPoint,
axisFontSize,
});
// TODO: Make this dynamic based on the data
const maxLeftAxisTickLabelLength = MAX_LEFT_AXIS_LABEL_LENGTH;
const maxLeftAxisTickLabelLength = computeMaxLabelLengthForMargin({
marginSize: margins.left,
axisFontSize,
});
return {
categoryTickValues,
numberOfValueTicks,
maxBottomAxisTickLabelLength,
maxBottomAxisTickLabelLength: tickRotationConfig.maxLabelLength,
maxLeftAxisTickLabelLength,
bottomAxisTickRotation: tickRotationConfig.tickRotation,
};
};
@@ -3,7 +3,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { getGroupByQueryResultGqlFieldName } from '@/page-layout/utils/getGroupByQueryResultGqlFieldName';
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 { BarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartLayout';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { fillDateGapsInBarChartData } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/fillDateGapsInBarChartData';
@@ -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 { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
import { type GroupByRawResult } from '@/page-layout/widgets/graph/types/GroupByRawResult';
@@ -56,7 +56,10 @@ export const transformOneDimensionalGroupByToBarChartData = ({
: aggregateField.name;
// TODO: Add a limit to the query instead of slicing here (issue: twentyhq/core-team-issues#1600)
const limitedResults = rawResults.slice(0, BAR_CHART_MAXIMUM_NUMBER_OF_BARS);
const limitedResults = rawResults.slice(
0,
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS,
);
const formattedValues = formatPrimaryDimensionValues({
groupByRawResults: limitedResults,
@@ -119,7 +122,8 @@ export const transformOneDimensionalGroupByToBarChartData = ({
indexBy: indexByKey,
keys: [aggregateValueKey],
series,
hasTooManyGroups: rawResults.length > BAR_CHART_MAXIMUM_NUMBER_OF_BARS,
hasTooManyGroups:
rawResults.length > BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS,
formattedToRawLookup,
};
};
@@ -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 { BAR_CHART_MAXIMUM_NUMBER_OF_BARS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartMaximumNumberOfBars.constant';
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
import { type BarChartSeries } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSeries';
import { sortBarChartDataBySecondaryDimensionSum } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/sortBarChartDataBySecondaryDimensionSum';
import { type GraphColor } from '@/page-layout/widgets/graph/types/GraphColor';
@@ -97,7 +97,10 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
const isNewY = !yValues.has(yValue);
if (configuration.groupMode === BarChartGroupMode.STACKED) {
if (isNewX && xValues.size >= BAR_CHART_MAXIMUM_NUMBER_OF_BARS) {
if (
isNewX &&
xValues.size >= BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS
) {
hasTooManyGroups = true;
return;
}
@@ -110,7 +113,7 @@ export const transformTwoDimensionalGroupByToBarChartData = ({
if (
totalUniqueDimensions + additionalDimensions >
BAR_CHART_MAXIMUM_NUMBER_OF_BARS
BAR_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_BARS
) {
hasTooManyGroups = true;
return;
@@ -1,10 +1,18 @@
import { ELLIPSIS_LENGTH } from '@/page-layout/widgets/graph/utils/ellipsisLength';
export const truncateTickLabel = (
value: string | number,
maxLength: number,
): string => {
const stringValue = String(value);
if (maxLength < 4 || stringValue.length <= maxLength) {
if (stringValue.length <= maxLength) {
return stringValue;
}
return `${stringValue.slice(0, maxLength - 3)}...`;
if (maxLength <= ELLIPSIS_LENGTH) {
return '...'.slice(0, maxLength);
}
return `${stringValue.slice(0, maxLength - ELLIPSIS_LENGTH)}...`;
};
@@ -1,10 +1,4 @@
import { LINE_CHART_CROSSHAIR_DASH_ARRAY } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartCrosshairDashArray';
import { LINE_CHART_CROSSHAIR_STROKE_OPACITY } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartCrosshairStrokeOpacity';
import { LINE_CHART_CROSSHAIR_STROKE_WIDTH } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartCrosshairStrokeWidth';
import { LINE_CHART_CROSSHAIR_TRANSITION_DAMPING } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartCrosshairTransitionDamping';
import { LINE_CHART_CROSSHAIR_TRANSITION_STIFFNESS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartCrosshairTransitionStiffness';
import { LINE_CHART_MARGIN_LEFT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginLeft';
import { LINE_CHART_MARGIN_TOP } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginTop';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { graphWidgetLineCrosshairXComponentState } from '@/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineCrosshairXComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useTheme } from '@emotion/react';
@@ -29,6 +23,8 @@ type CustomCrosshairLayerProps = {
points: readonly Point<LineSeries>[];
innerHeight: number;
innerWidth: number;
marginLeft: number;
marginTop: number;
onSliceHover: (data: SliceHoverData) => void;
onSliceClick?: (data: SliceHoverData) => void;
onRectLeave: (relatedTarget: EventTarget | null) => void;
@@ -38,6 +34,8 @@ export const CustomCrosshairLayer = ({
points,
innerHeight,
innerWidth,
marginLeft,
marginTop,
onSliceHover,
onSliceClick,
onRectLeave,
@@ -75,8 +73,8 @@ export const CustomCrosshairLayer = ({
return null;
}
const mouseX = event.clientX - svgRect.left - LINE_CHART_MARGIN_LEFT;
const mouseY = event.clientY - svgRect.top - LINE_CHART_MARGIN_TOP;
const mouseX = event.clientX - svgRect.left - marginLeft;
const mouseY = event.clientY - svgRect.top - marginTop;
const nearestSlice = slices.reduce((nearest, slice) => {
const currentDistance = Math.abs(slice.x - mouseX);
@@ -102,7 +100,7 @@ export const CustomCrosshairLayer = ({
svgRect,
};
},
[slices],
[marginLeft, marginTop, slices],
);
const handleMouseMove = (event: MouseEvent<SVGRectElement>) => {
@@ -136,8 +134,8 @@ export const CustomCrosshairLayer = ({
const transition = {
type: 'spring',
stiffness: LINE_CHART_CROSSHAIR_TRANSITION_STIFFNESS,
damping: LINE_CHART_CROSSHAIR_TRANSITION_DAMPING,
stiffness: LINE_CHART_CONSTANTS.CROSSHAIR_TRANSITION_STIFFNESS,
damping: LINE_CHART_CONSTANTS.CROSSHAIR_TRANSITION_DAMPING,
} as const;
return (
@@ -149,9 +147,9 @@ export const CustomCrosshairLayer = ({
y1={0}
y2={innerHeight}
stroke={theme.font.color.primary}
strokeWidth={LINE_CHART_CROSSHAIR_STROKE_WIDTH}
strokeOpacity={LINE_CHART_CROSSHAIR_STROKE_OPACITY}
strokeDasharray={LINE_CHART_CROSSHAIR_DASH_ARRAY}
strokeWidth={LINE_CHART_CONSTANTS.CROSSHAIR_STROKE_WIDTH}
strokeOpacity={LINE_CHART_CONSTANTS.CROSSHAIR_STROKE_OPACITY}
strokeDasharray={LINE_CHART_CONSTANTS.CROSSHAIR_DASH_ARRAY}
initial={{ x1: crosshairX, x2: crosshairX, opacity: 0 }}
animate={{ x1: crosshairX, x2: crosshairX, opacity: 0.5 }}
exit={{ opacity: 0 }}
@@ -1,5 +1,5 @@
import { GraphWidgetFloatingTooltip } from '@/page-layout/widgets/graph/components/GraphWidgetFloatingTooltip';
import { LINE_CHART_TOOLTIP_OFFSET_PX } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartTooltipOffsetPx';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { graphWidgetLineTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineTooltipComponentState';
import { type LineChartEnrichedSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartEnrichedSeries';
import { getLineChartTooltipData } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartTooltipData';
@@ -71,7 +71,7 @@ export const GraphLineChartTooltip = ({
<GraphWidgetFloatingTooltip
reference={reference}
boundary={containerElement}
tooltipOffsetFromAnchorInPx={LINE_CHART_TOOLTIP_OFFSET_PX}
tooltipOffsetFromAnchorInPx={LINE_CHART_CONSTANTS.TOOLTIP_OFFSET_PX}
items={tooltipData?.items ?? []}
indexLabel={tooltipData?.indexLabel}
highlightedKey={tooltipState?.highlightedSeriesId}
@@ -1,6 +1,7 @@
import { GraphWidgetChartContainer } from '@/page-layout/widgets/graph/components/GraphWidgetChartContainer';
import { GraphWidgetLegend } from '@/page-layout/widgets/graph/components/GraphWidgetLegend';
import { NoDataLayer } from '@/page-layout/widgets/graph/components/NoDataLayer';
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
import {
CustomCrosshairLayer,
type SliceHoverData,
@@ -9,10 +10,7 @@ import { CustomLinesLayer } from '@/page-layout/widgets/graph/graphWidgetLineCha
import { CustomPointLabelsLayer } from '@/page-layout/widgets/graph/graphWidgetLineChart/components/CustomPointLabelsLayer';
import { CustomStackedAreasLayer } from '@/page-layout/widgets/graph/graphWidgetLineChart/components/CustomStackedAreasLayer';
import { GraphLineChartTooltip } from '@/page-layout/widgets/graph/graphWidgetLineChart/components/GraphLineChartTooltip';
import { LINE_CHART_MARGIN_BOTTOM } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginBottom';
import { LINE_CHART_MARGIN_LEFT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginLeft';
import { LINE_CHART_MARGIN_RIGHT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginRight';
import { LINE_CHART_MARGIN_TOP } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginTop';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { useLineChartData } from '@/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartData';
import { useLineChartTheme } from '@/page-layout/widgets/graph/graphWidgetLineChart/hooks/useLineChartTheme';
import { graphWidgetLineCrosshairXComponentState } from '@/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineCrosshairXComponentState';
@@ -49,8 +47,6 @@ type StackedAreasLayerProps = LineCustomSvgLayerProps<LineSeries>;
type LinesLayerProps = LineCustomSvgLayerProps<LineSeries>;
type NoDataLayerWrapperProps = LineCustomSvgLayerProps<LineSeries>;
const LINE_CHART_DEFAULT_TICK_COUNT = 5;
type GraphWidgetLineChartProps = {
data: LineChartSeries[];
showLegend?: boolean;
@@ -171,8 +167,8 @@ export const GraphWidgetLineChart = ({
points: sliceData.nearestSlice.points,
};
const offsetLeft = sliceData.nearestSlice.x + LINE_CHART_MARGIN_LEFT;
const offsetTop = sliceData.mouseY + LINE_CHART_MARGIN_TOP;
const offsetLeft = sliceData.nearestSlice.x + marginLeft;
const offsetTop = sliceData.mouseY + COMMON_CHART_CONSTANTS.MARGIN_TOP;
debouncedHideTooltip.cancel();
setCrosshairX(sliceData.sliceX);
@@ -212,6 +208,8 @@ export const GraphWidgetLineChart = ({
points={layerProps.points}
innerHeight={layerProps.innerHeight}
innerWidth={layerProps.innerWidth}
marginLeft={marginLeft}
marginTop={COMMON_CHART_CONSTANTS.MARGIN_TOP}
onSliceHover={handleSliceEnter}
onSliceClick={
isDefined(onSliceClick)
@@ -262,29 +260,24 @@ export const GraphWidgetLineChart = ({
/>
);
const axisBottomConfig = getLineChartAxisBottomConfig(
xAxisLabel,
chartWidth,
visibleData,
);
const chartMargins = {
top: LINE_CHART_MARGIN_TOP,
right: LINE_CHART_MARGIN_RIGHT,
bottom: LINE_CHART_MARGIN_BOTTOM,
left: LINE_CHART_MARGIN_LEFT,
};
const marginLeft = isDefined(yAxisLabel)
? COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITH_LABEL
: COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITHOUT_LABEL;
const { config: axisBottomConfig, marginBottom } =
getLineChartAxisBottomConfig(xAxisLabel, chartWidth, data, marginLeft);
const { tickValues: valueTickValues, domain: valueDomain } =
computeValueTickValues({
minimum: effectiveMinimumValue,
maximum: effectiveMaximumValue,
tickCount: LINE_CHART_DEFAULT_TICK_COUNT,
tickCount: LINE_CHART_CONSTANTS.DEFAULT_TICK_COUNT,
});
const axisLeftConfig = getLineChartAxisLeftConfig(
yAxisLabel,
formatOptions,
valueTickValues,
chartMargins.left,
);
return (
@@ -303,10 +296,10 @@ export const GraphWidgetLineChart = ({
<ResponsiveLine
data={nivoData}
margin={{
top: chartMargins.top,
right: chartMargins.right,
bottom: chartMargins.bottom,
left: chartMargins.left,
top: COMMON_CHART_CONSTANTS.MARGIN_TOP,
right: COMMON_CHART_CONSTANTS.MARGIN_RIGHT,
bottom: marginBottom,
left: marginLeft,
}}
xScale={{ type: 'point' }}
yScale={{
@@ -2,7 +2,7 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPag
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader';
import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect';
import { LINE_CHART_IS_STACKED_DEFAULT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartIsStackedDefault';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { useGraphLineChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData';
import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint';
import { assertLineChartWidgetOrThrow } from '@/page-layout/widgets/graph/utils/assertLineChartWidget';
@@ -59,7 +59,7 @@ export const GraphWidgetLineChartRenderer = ({
const groupMode =
hasGroupByOnSecondaryAxis &&
(configuration.isStacked ?? LINE_CHART_IS_STACKED_DEFAULT)
(configuration.isStacked ?? LINE_CHART_CONSTANTS.IS_STACKED_DEFAULT)
? 'stacked'
: undefined;
@@ -1 +0,0 @@
export const LINE_AREA_FILL_END_OPACITY = 0.01;
@@ -1 +0,0 @@
export const LINE_AREA_FILL_START_OPACITY = 0.18;
@@ -0,0 +1,18 @@
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
export const LINE_CHART_CONSTANTS = {
...COMMON_CHART_CONSTANTS,
MAXIMUM_NUMBER_OF_DATA_POINTS: 100,
MAXIMUM_NUMBER_OF_NON_STACKED_SERIES: 50,
MAXIMUM_NUMBER_OF_STACKED_SERIES: 50,
TOOLTIP_OFFSET_PX: 4,
IS_STACKED_DEFAULT: true,
AREA_FILL_START_OPACITY: 0.18,
AREA_FILL_END_OPACITY: 0.01,
CROSSHAIR_STROKE_WIDTH: 1,
CROSSHAIR_STROKE_OPACITY: 0.5,
CROSSHAIR_DASH_ARRAY: '4 4',
CROSSHAIR_TRANSITION_DAMPING: 30,
CROSSHAIR_TRANSITION_STIFFNESS: 500,
DEFAULT_TICK_COUNT: 5,
} as const;
@@ -1 +0,0 @@
export const LINE_CHART_CROSSHAIR_DASH_ARRAY = '4 4';
@@ -1 +0,0 @@
export const LINE_CHART_CROSSHAIR_STROKE_OPACITY = 0.5;
@@ -1 +0,0 @@
export const LINE_CHART_CROSSHAIR_STROKE_WIDTH = 1;
@@ -1 +0,0 @@
export const LINE_CHART_CROSSHAIR_TRANSITION_DAMPING = 30;
@@ -1 +0,0 @@
export const LINE_CHART_CROSSHAIR_TRANSITION_STIFFNESS = 500;
@@ -1 +0,0 @@
export const LINE_CHART_IS_STACKED_DEFAULT = true;
@@ -1 +0,0 @@
export const LINE_CHART_MARGIN_BOTTOM = 60;
@@ -1 +0,0 @@
export const LINE_CHART_MARGIN_LEFT = 80;
@@ -1 +0,0 @@
export const LINE_CHART_MARGIN_RIGHT = 20;
@@ -1 +0,0 @@
export const LINE_CHART_MARGIN_TOP = 20;
@@ -1 +0,0 @@
export const LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS = 100;
@@ -1 +0,0 @@
export const LINE_CHART_MAXIMUM_NUMBER_OF_NON_STACKED_SERIES = 50;
@@ -1 +0,0 @@
export const LINE_CHART_MAXIMUM_NUMBER_OF_STACKED_SERIES = 50;
@@ -1 +0,0 @@
export const LINE_CHART_TOOLTIP_OFFSET_PX = 4;
@@ -1,7 +1,5 @@
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups.constant';
import { LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfDataPoints.constant';
import { LINE_CHART_MAXIMUM_NUMBER_OF_NON_STACKED_SERIES } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfNonStackedSeries.constant';
import { LINE_CHART_MAXIMUM_NUMBER_OF_STACKED_SERIES } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfStackedSeries.constant';
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { type LineChartConfiguration } from '~/generated-metadata/graphql';
import { getLineChartQueryLimit } from '../getLineChartQueryLimit';
@@ -14,7 +12,7 @@ describe('getLineChartQueryLimit', () => {
} as LineChartConfiguration);
expect(result).toBe(
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS +
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
);
});
@@ -27,8 +25,8 @@ describe('getLineChartQueryLimit', () => {
} as LineChartConfiguration);
expect(result).toBe(
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS *
LINE_CHART_MAXIMUM_NUMBER_OF_STACKED_SERIES +
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS *
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_STACKED_SERIES +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
);
});
@@ -41,8 +39,8 @@ describe('getLineChartQueryLimit', () => {
} as LineChartConfiguration);
expect(result).toBe(
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS *
LINE_CHART_MAXIMUM_NUMBER_OF_NON_STACKED_SERIES +
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS *
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_NON_STACKED_SERIES +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS,
);
});
@@ -1,16 +1,17 @@
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
import { computeChartCategoryTickValues } from '@/page-layout/widgets/graph/utils/computeChartCategoryTickValues';
const LINE_CHART_MARGIN_LEFT = 70;
const LINE_CHART_MARGIN_RIGHT = 20;
const LINE_CHART_MINIMUM_WIDTH_PER_TICK = 100;
export const computeLineChartCategoryTickValues = ({
width,
data,
marginLeft,
marginRight,
}: {
width: number;
data: LineChartSeries[];
marginLeft: number;
marginRight: number;
}): (string | number)[] => {
if (data.length === 0 || data[0].data.length === 0) {
return [];
@@ -23,12 +24,15 @@ export const computeLineChartCategoryTickValues = ({
: String(value);
});
const availableWidth =
width - (LINE_CHART_MARGIN_LEFT + LINE_CHART_MARGIN_RIGHT);
const availableWidth = width - (marginLeft + marginRight);
const dataPointCount = data[0].data.length;
const widthPerTick = dataPointCount > 0 ? availableWidth / dataPointCount : 0;
return computeChartCategoryTickValues({
availableSize: availableWidth,
minimumSizePerTick: LINE_CHART_MINIMUM_WIDTH_PER_TICK,
minimumSizePerTick: LINE_CHART_CONSTANTS.MINIMUM_WIDTH_PER_TICK_ROTATED,
values,
widthPerTick,
});
};
@@ -1,5 +1,4 @@
import { LINE_AREA_FILL_END_OPACITY } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineAreaFillEndOpacity';
import { LINE_AREA_FILL_START_OPACITY } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineAreaFillStartOpacity';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { type GraphColorScheme } from '@/page-layout/widgets/graph/types/GraphColorScheme';
export const createAreaFillDef = (
@@ -17,12 +16,12 @@ export const createAreaFillDef = (
{
offset: 0,
color: colorScheme.solid,
opacity: LINE_AREA_FILL_START_OPACITY,
opacity: LINE_CHART_CONSTANTS.AREA_FILL_START_OPACITY,
},
{
offset: 100,
color: colorScheme.solid,
opacity: LINE_AREA_FILL_END_OPACITY,
opacity: LINE_CHART_CONSTANTS.AREA_FILL_END_OPACITY,
},
],
};
@@ -1,23 +1,87 @@
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
import { truncateTickLabel } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel';
import { type LineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeries';
import { computeLineChartCategoryTickValues } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/computeLineChartCategoryTickValues';
import { getTickRotationConfig } from '@/page-layout/widgets/graph/utils/getTickRotationConfig';
import { isNonEmptyArray } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
export type LineChartAxisBottomResult = {
config: {
tickSize: number;
tickPadding: number;
tickRotation: number;
tickValues: (string | number)[] | undefined;
legend: string | undefined;
legendPosition: 'middle';
legendOffset: number;
format: (value: string | number) => string;
};
marginBottom: number;
};
export const getLineChartAxisBottomConfig = (
xAxisLabel?: string,
width?: number,
data?: LineChartSeries[],
) => {
marginLeft?: number,
): LineChartAxisBottomResult => {
const effectiveMarginLeft =
marginLeft ?? COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITH_LABEL;
const tickValues =
width && data
? computeLineChartCategoryTickValues({ width, data })
? computeLineChartCategoryTickValues({
width,
data,
marginLeft: effectiveMarginLeft,
marginRight: COMMON_CHART_CONSTANTS.MARGIN_RIGHT,
})
: undefined;
const availableWidth = width
? width - (effectiveMarginLeft + COMMON_CHART_CONSTANTS.MARGIN_RIGHT)
: 0;
const actualDataPointCount = isNonEmptyArray(data?.[0]?.data)
? data[0].data.length
: 1;
const widthPerDataPoint =
actualDataPointCount > 0 && availableWidth > 0
? availableWidth / actualDataPointCount
: 0;
const { tickRotation, maxLabelLength } = getTickRotationConfig({
widthPerTick: widthPerDataPoint,
axisFontSize: COMMON_CHART_CONSTANTS.AXIS_FONT_SIZE,
});
const hasRotation = tickRotation !== 0;
const baseMarginBottom = isDefined(xAxisLabel)
? COMMON_CHART_CONSTANTS.MARGIN_BOTTOM_WITH_LABEL
: COMMON_CHART_CONSTANTS.MARGIN_BOTTOM_WITHOUT_LABEL;
const marginBottom = hasRotation
? baseMarginBottom +
COMMON_CHART_CONSTANTS.ROTATED_LABELS_EXTRA_BOTTOM_MARGIN
: baseMarginBottom;
return {
tickSize: 0,
tickPadding: 5,
tickRotation: 0,
tickValues,
legend: xAxisLabel,
legendPosition: 'middle' as const,
legendOffset: 40,
config: {
tickSize: COMMON_CHART_CONSTANTS.TICK_SIZE,
tickPadding: COMMON_CHART_CONSTANTS.TICK_PADDING,
tickRotation,
tickValues,
legend: xAxisLabel,
legendPosition: 'middle' as const,
legendOffset:
COMMON_CHART_CONSTANTS.BOTTOM_AXIS_LEGEND_OFFSET +
(hasRotation
? COMMON_CHART_CONSTANTS.ROTATED_LABELS_EXTRA_BOTTOM_MARGIN
: 0),
format: (value: string | number) =>
truncateTickLabel(String(value), maxLabelLength),
},
marginBottom,
};
};
@@ -1,24 +1,32 @@
import { LINE_CHART_MARGIN_LEFT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMarginLeft';
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
import {
formatGraphValue,
type GraphValueFormatOptions,
} from '@/page-layout/widgets/graph/utils/graphFormatters';
const LEFT_AXIS_LEGEND_OFFSET_PADDING = 5;
const TICK_PADDING = 5;
import { isDefined } from 'twenty-shared/utils';
export const getLineChartAxisLeftConfig = (
yAxisLabel?: string,
formatOptions?: GraphValueFormatOptions,
tickValues?: number[],
marginLeft: number = LINE_CHART_MARGIN_LEFT,
) => ({
tickSize: 0,
tickPadding: TICK_PADDING,
tickRotation: 0,
tickValues,
legend: yAxisLabel,
legendPosition: 'middle' as const,
legendOffset: -marginLeft + LEFT_AXIS_LEGEND_OFFSET_PADDING,
format: (value: number) => formatGraphValue(value, formatOptions || {}),
});
marginLeft?: number,
) => {
const effectiveMarginLeft =
marginLeft ??
(isDefined(yAxisLabel)
? COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITH_LABEL
: COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITHOUT_LABEL);
return {
tickSize: COMMON_CHART_CONSTANTS.TICK_SIZE,
tickPadding: COMMON_CHART_CONSTANTS.TICK_PADDING,
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
tickValues,
legend: yAxisLabel,
legendPosition: 'middle' as const,
legendOffset:
-effectiveMarginLeft +
COMMON_CHART_CONSTANTS.LEFT_AXIS_LEGEND_OFFSET_PADDING,
format: (value: number) => formatGraphValue(value, formatOptions || {}),
};
};
@@ -1,7 +1,5 @@
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups.constant';
import { LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfDataPoints.constant';
import { LINE_CHART_MAXIMUM_NUMBER_OF_NON_STACKED_SERIES } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfNonStackedSeries.constant';
import { LINE_CHART_MAXIMUM_NUMBER_OF_STACKED_SERIES } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartMaximumNumberOfStackedSeries.constant';
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups';
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
import { isChartConfigurationTwoDimensional } from '@/page-layout/widgets/graph/utils/isChartConfigurationTwoDimensional';
import { type LineChartConfiguration } from '~/generated/graphql';
@@ -12,22 +10,22 @@ export const getLineChartQueryLimit = (
if (!isTwoDimensional) {
return (
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS +
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS
);
}
if (configuration.isStacked === true) {
return (
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS *
LINE_CHART_MAXIMUM_NUMBER_OF_STACKED_SERIES +
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS *
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_STACKED_SERIES +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS
);
}
return (
LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS *
LINE_CHART_MAXIMUM_NUMBER_OF_NON_STACKED_SERIES +
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_DATA_POINTS *
LINE_CHART_CONSTANTS.MAXIMUM_NUMBER_OF_NON_STACKED_SERIES +
EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS
);
};
@@ -1,7 +1,7 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups.constant';
import { EXTRA_ITEM_TO_DETECT_TOO_MANY_GROUPS } from '@/page-layout/widgets/graph/constants/ExtraItemToDetectTooManyGroups';
import { PIE_CHART_MAXIMUM_NUMBER_OF_SLICES } from '@/page-layout/widgets/graph/graphWidgetPieChart/constants/PieChartMaximumNumberOfSlices.constant';
import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem';
import { transformGroupByDataToPieChartData } from '@/page-layout/widgets/graph/graphWidgetPieChart/utils/transformGroupByDataToPieChartData';
@@ -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);
});
});
});
@@ -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);
});
});
@@ -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,
@@ -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';
@@ -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);
@@ -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;
}
}
@@ -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,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,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,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 {
@@ -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,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,
@@ -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,
};
};
@@ -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,
};
};
@@ -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;
}