[Dashboards] Dynamic paddings (#17298)
closes https://github.com/twentyhq/core-team-issues/issues/2055 https://github.com/user-attachments/assets/7a19066f-e891-4dfa-92ee-b85690765e60
This commit is contained in:
+4
-6
@@ -1,24 +1,22 @@
|
||||
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,
|
||||
ROTATED_TICK_MINIMUM_NUMBER_OF_DISPLAYED_CHARACTERS: 12,
|
||||
HORIZONTAL_LABEL_CHARACTER_WIDTH_RATIO: 0.6,
|
||||
ROTATED_LABEL_CHARACTER_WIDTH_RATIO: 0.5,
|
||||
ROTATED_TICK_LABEL_WIDTH_ESTIMATE_RATIO: 0.5,
|
||||
TICK_LABEL_LINE_HEIGHT_RATIO: 1.4,
|
||||
TICK_ROTATION_ANGLE: -45,
|
||||
TICK_PADDING_ALLOWANCE: 10,
|
||||
MIN_TICK_LABEL_LENGTH: 5,
|
||||
POSITIVE_RANGE_PADDING_RATIO: 0.1,
|
||||
MINIMUM_POSITIVE_RANGE_PADDING: 1,
|
||||
LEGEND_OFFSET_MARGIN_BUFFER: 4,
|
||||
NO_ROTATION_ANGLE: 0,
|
||||
} as const;
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const TEXT_MARGIN_EXTRAS = {
|
||||
tickPaddingExtra: 4,
|
||||
bottomTickExtraNonRotated: 20,
|
||||
} as const;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export const TEXT_MARGIN_LIMITS = {
|
||||
min: { top: 10, right: 10, bottom: 30, left: 40 },
|
||||
max: { top: 30, right: 30, bottom: 140, left: 160 },
|
||||
} as const;
|
||||
+17
-30
@@ -14,13 +14,11 @@ import { type BarChartSeriesWithColor } from '@/page-layout/widgets/graph/graphW
|
||||
import { type BarChartSlice } from '@/page-layout/widgets/graph/graphWidgetBarChart/types/BarChartSlice';
|
||||
import { calculateStackedBarChartValueRange } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateStackedBarChartValueRange';
|
||||
import { calculateValueRangeFromBarChartKeys } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateValueRangeFromBarChartKeys';
|
||||
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 { getBarChartTickConfig } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig';
|
||||
import { getBarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartLayout';
|
||||
import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode';
|
||||
import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange';
|
||||
import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues';
|
||||
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
|
||||
import {
|
||||
formatGraphValue,
|
||||
@@ -165,24 +163,26 @@ export const GraphWidgetBarChart = ({
|
||||
rangeMax,
|
||||
});
|
||||
|
||||
const tickConfig = getBarChartTickConfig({
|
||||
width: chartWidth,
|
||||
height: chartHeight,
|
||||
const {
|
||||
margins,
|
||||
axisBottomConfiguration,
|
||||
axisLeftConfiguration,
|
||||
valueTickValues,
|
||||
valueDomain,
|
||||
} = getBarChartLayout({
|
||||
axisTheme: chartTheme.axis,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
data,
|
||||
indexBy,
|
||||
layout,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
axisFontSize: chartTheme.axis.ticks.text.fontSize,
|
||||
layout,
|
||||
formatOptions,
|
||||
effectiveMinimumValue,
|
||||
effectiveMaximumValue,
|
||||
});
|
||||
|
||||
const { tickValues: valueTickValues, domain: valueDomain } =
|
||||
computeValueTickValues({
|
||||
minimum: effectiveMinimumValue,
|
||||
maximum: effectiveMaximumValue,
|
||||
tickCount: tickConfig.numberOfValueTicks,
|
||||
});
|
||||
|
||||
const hasClickableItems = isDefined(onSliceClick);
|
||||
|
||||
const hideTooltip = () => {
|
||||
@@ -222,19 +222,6 @@ export const GraphWidgetBarChart = ({
|
||||
debouncedHideTooltip();
|
||||
};
|
||||
|
||||
const {
|
||||
axisBottom: axisBottomConfig,
|
||||
axisLeft: axisLeftConfig,
|
||||
margins,
|
||||
} = getBarChartAxisConfigs({
|
||||
layout,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
valueTickValues,
|
||||
tickConfig,
|
||||
});
|
||||
|
||||
const BarItemWithContext = useMemo(
|
||||
() => (props: BarItemProps<BarDatum>) => (
|
||||
<CustomBarItem
|
||||
@@ -355,8 +342,8 @@ export const GraphWidgetBarChart = ({
|
||||
markers={zeroMarker}
|
||||
axisTop={null}
|
||||
axisRight={null}
|
||||
axisBottom={axisBottomConfig}
|
||||
axisLeft={axisLeftConfig}
|
||||
axisBottom={axisBottomConfiguration}
|
||||
axisLeft={axisLeftConfiguration}
|
||||
enableGridX={layout === BarChartLayout.HORIZONTAL && showGrid}
|
||||
enableGridY={layout === BarChartLayout.VERTICAL && showGrid}
|
||||
gridXValues={
|
||||
|
||||
+6
-3
@@ -1,7 +1,10 @@
|
||||
import { parseFontSizeToPx } from '@/page-layout/widgets/graph/utils/parseFontSizeToPx';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
export const useBarChartTheme = () => {
|
||||
const theme = useTheme();
|
||||
const tickFontSize = 11;
|
||||
const legendFontSize = parseFontSizeToPx(theme.font.size.sm, tickFontSize);
|
||||
|
||||
return {
|
||||
axis: {
|
||||
@@ -18,13 +21,13 @@ export const useBarChartTheme = () => {
|
||||
},
|
||||
text: {
|
||||
fill: theme.font.color.secondary,
|
||||
fontSize: 11,
|
||||
fontSize: tickFontSize,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
text: {
|
||||
fill: theme.font.color.primary,
|
||||
fontSize: theme.font.size.sm,
|
||||
fontSize: legendFontSize,
|
||||
fontWeight: theme.font.weight.medium,
|
||||
},
|
||||
},
|
||||
@@ -38,7 +41,7 @@ export const useBarChartTheme = () => {
|
||||
},
|
||||
labels: {
|
||||
text: {
|
||||
fontSize: 11,
|
||||
fontSize: tickFontSize,
|
||||
fontWeight: theme.font.weight.medium,
|
||||
},
|
||||
},
|
||||
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
import { calculateWidthPerTick } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/calculateWidthPerTick';
|
||||
import { BarChartLayout } from '~/generated/graphql';
|
||||
|
||||
describe('calculateWidthPerTick', () => {
|
||||
describe('vertical layout', () => {
|
||||
it('should calculate width per tick based on categoryTickCount for vertical layout', () => {
|
||||
const result = calculateWidthPerTick({
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
availableWidth: 500,
|
||||
categoryTickCount: 10,
|
||||
valueTickCount: 5,
|
||||
});
|
||||
|
||||
expect(result).toBe(50);
|
||||
});
|
||||
|
||||
it('should return 0 when categoryTickCount is 0 for vertical layout', () => {
|
||||
const result = calculateWidthPerTick({
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
availableWidth: 500,
|
||||
categoryTickCount: 0,
|
||||
valueTickCount: 5,
|
||||
});
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle small available width', () => {
|
||||
const result = calculateWidthPerTick({
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
availableWidth: 100,
|
||||
categoryTickCount: 20,
|
||||
valueTickCount: 5,
|
||||
});
|
||||
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizontal layout', () => {
|
||||
it('should calculate width per tick based on valueTickCount for horizontal layout', () => {
|
||||
const result = calculateWidthPerTick({
|
||||
layout: BarChartLayout.HORIZONTAL,
|
||||
availableWidth: 600,
|
||||
categoryTickCount: 10,
|
||||
valueTickCount: 6,
|
||||
});
|
||||
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
|
||||
it('should return 0 when valueTickCount is 0 for horizontal layout', () => {
|
||||
const result = calculateWidthPerTick({
|
||||
layout: BarChartLayout.HORIZONTAL,
|
||||
availableWidth: 500,
|
||||
categoryTickCount: 10,
|
||||
valueTickCount: 0,
|
||||
});
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle decimal results', () => {
|
||||
const result = calculateWidthPerTick({
|
||||
layout: BarChartLayout.HORIZONTAL,
|
||||
availableWidth: 100,
|
||||
categoryTickCount: 10,
|
||||
valueTickCount: 3,
|
||||
});
|
||||
|
||||
expect(result).toBeCloseTo(33.33, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle availableWidth of 0', () => {
|
||||
const result = calculateWidthPerTick({
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
availableWidth: 0,
|
||||
categoryTickCount: 10,
|
||||
valueTickCount: 5,
|
||||
});
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle single tick', () => {
|
||||
const result = calculateWidthPerTick({
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
availableWidth: 500,
|
||||
categoryTickCount: 1,
|
||||
valueTickCount: 5,
|
||||
});
|
||||
|
||||
expect(result).toBe(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import { TEXT_MARGIN_LIMITS } from '@/page-layout/widgets/graph/constants/TextMarginLimits';
|
||||
import { getBarChartLayout } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartLayout';
|
||||
import { type ChartAxisTheme } from '@/page-layout/widgets/graph/types/ChartAxisTheme';
|
||||
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { BarChartLayout } from '~/generated/graphql';
|
||||
|
||||
describe('getBarChartLayout', () => {
|
||||
const defaultAxisTheme: ChartAxisTheme = {
|
||||
ticks: { text: { fontSize: 11 } },
|
||||
legend: { text: { fontSize: 11 } },
|
||||
};
|
||||
|
||||
const defaultFormatOptions: GraphValueFormatOptions = {
|
||||
displayType: 'number',
|
||||
};
|
||||
|
||||
const defaultData: BarDatum[] = [
|
||||
{ category: 'A', value: 10 },
|
||||
{ category: 'B', value: 20 },
|
||||
{ category: 'C', value: 30 },
|
||||
];
|
||||
|
||||
const baseParams = {
|
||||
axisTheme: defaultAxisTheme,
|
||||
chartWidth: 500,
|
||||
chartHeight: 300,
|
||||
data: defaultData,
|
||||
indexBy: 'category',
|
||||
xAxisLabel: 'Categories',
|
||||
yAxisLabel: 'Values',
|
||||
formatOptions: defaultFormatOptions,
|
||||
effectiveMinimumValue: 0,
|
||||
effectiveMaximumValue: 30,
|
||||
};
|
||||
|
||||
describe('vertical layout', () => {
|
||||
it('produces correct axis configurations', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
});
|
||||
|
||||
expect(result.axisBottomConfiguration).toBeDefined();
|
||||
expect(result.axisLeftConfiguration).toBeDefined();
|
||||
expect(result.valueTickValues).toBeDefined();
|
||||
expect(result.valueDomain).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns margins within TEXT_MARGIN_LIMITS bounds', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
});
|
||||
|
||||
expect(result.margins.top).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.top,
|
||||
);
|
||||
expect(result.margins.top).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.top,
|
||||
);
|
||||
expect(result.margins.right).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.right,
|
||||
);
|
||||
expect(result.margins.right).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.right,
|
||||
);
|
||||
expect(result.margins.bottom).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.bottom,
|
||||
);
|
||||
expect(result.margins.bottom).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.bottom,
|
||||
);
|
||||
expect(result.margins.left).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.left,
|
||||
);
|
||||
expect(result.margins.left).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.left,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('horizontal layout', () => {
|
||||
it('produces correct axis configurations', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
layout: BarChartLayout.HORIZONTAL,
|
||||
});
|
||||
|
||||
expect(result.axisBottomConfiguration).toBeDefined();
|
||||
expect(result.axisLeftConfiguration).toBeDefined();
|
||||
expect(result.valueTickValues).toBeDefined();
|
||||
expect(result.valueDomain).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns margins within TEXT_MARGIN_LIMITS bounds', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
layout: BarChartLayout.HORIZONTAL,
|
||||
});
|
||||
|
||||
expect(result.margins.top).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.top,
|
||||
);
|
||||
expect(result.margins.top).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.top,
|
||||
);
|
||||
expect(result.margins.right).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.right,
|
||||
);
|
||||
expect(result.margins.right).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.right,
|
||||
);
|
||||
expect(result.margins.bottom).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.bottom,
|
||||
);
|
||||
expect(result.margins.bottom).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.bottom,
|
||||
);
|
||||
expect(result.margins.left).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.left,
|
||||
);
|
||||
expect(result.margins.left).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.left,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('handles empty data array', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
data: [],
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.axisBottomConfiguration).toBeDefined();
|
||||
expect(result.axisLeftConfiguration).toBeDefined();
|
||||
});
|
||||
|
||||
it('handles large value ranges with negative min and positive max', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
effectiveMinimumValue: -1000,
|
||||
effectiveMaximumValue: 5000,
|
||||
});
|
||||
|
||||
expect(result.valueDomain.min).toBeLessThanOrEqual(-1000);
|
||||
expect(result.valueDomain.max).toBeGreaterThanOrEqual(5000);
|
||||
expect(result.valueTickValues.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('handles single data point', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
data: [{ category: 'Single', value: 50 }],
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.valueTickValues.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('handles missing axis labels', () => {
|
||||
const result = getBarChartLayout({
|
||||
...baseParams,
|
||||
layout: BarChartLayout.VERTICAL,
|
||||
xAxisLabel: undefined,
|
||||
yAxisLabel: undefined,
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.axisBottomConfiguration).toBeDefined();
|
||||
expect(result.axisLeftConfiguration).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { BarChartLayout } from '~/generated/graphql';
|
||||
|
||||
export const calculateWidthPerTick = ({
|
||||
layout,
|
||||
availableWidth,
|
||||
categoryTickCount,
|
||||
valueTickCount,
|
||||
}: {
|
||||
layout: BarChartLayout;
|
||||
availableWidth: number;
|
||||
categoryTickCount: number;
|
||||
valueTickCount: number;
|
||||
}): number => {
|
||||
if (layout === BarChartLayout.VERTICAL) {
|
||||
return categoryTickCount > 0 ? availableWidth / categoryTickCount : 0;
|
||||
}
|
||||
|
||||
return valueTickCount > 0 ? availableWidth / valueTickCount : 0;
|
||||
};
|
||||
+3
-7
@@ -1,7 +1,7 @@
|
||||
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
|
||||
import { computeMinHeightPerTick } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/computeMinHeightPerTick';
|
||||
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
|
||||
import { computeChartCategoryTickValues } from '@/page-layout/widgets/graph/utils/computeChartCategoryTickValues';
|
||||
import { type ChartMargins } from '@/page-layout/widgets/graph/types/ChartMargins';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { BarChartLayout } from '~/generated/graphql';
|
||||
|
||||
@@ -10,8 +10,7 @@ export const computeBarChartCategoryTickValues = ({
|
||||
axisFontSize,
|
||||
data,
|
||||
indexBy,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
margins,
|
||||
layout,
|
||||
}: {
|
||||
axisSize: number;
|
||||
@@ -19,8 +18,7 @@ export const computeBarChartCategoryTickValues = ({
|
||||
data: BarDatum[];
|
||||
indexBy: string;
|
||||
layout: BarChartLayout;
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
margins: ChartMargins;
|
||||
}): (string | number)[] => {
|
||||
if (axisSize === 0 || data.length === 0) {
|
||||
return [];
|
||||
@@ -28,8 +26,6 @@ export const computeBarChartCategoryTickValues = ({
|
||||
|
||||
const values = data.map((item) => item[indexBy] as string | number);
|
||||
|
||||
const margins = getBarChartMargins({ xAxisLabel, yAxisLabel });
|
||||
|
||||
const totalMargins =
|
||||
layout === BarChartLayout.VERTICAL
|
||||
? margins.left + margins.right
|
||||
|
||||
+24
-21
@@ -1,4 +1,3 @@
|
||||
import { getBarChartMargins } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartMargins';
|
||||
import { truncateTickLabel } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel';
|
||||
import {
|
||||
formatGraphValue,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
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';
|
||||
import { type ChartMargins } from '@/page-layout/widgets/graph/types/ChartMargins';
|
||||
import { BarChartLayout } from '~/generated/graphql';
|
||||
|
||||
type GetBarChartAxisConfigsProps = {
|
||||
@@ -16,7 +16,8 @@ type GetBarChartAxisConfigsProps = {
|
||||
yAxisLabel?: string;
|
||||
formatOptions?: GraphValueFormatOptions;
|
||||
valueTickValues?: number[];
|
||||
tickConfig: BarChartTickConfig;
|
||||
tickConfiguration: BarChartTickConfig;
|
||||
margins: ChartMargins;
|
||||
};
|
||||
|
||||
export const getBarChartAxisConfigs = ({
|
||||
@@ -25,7 +26,8 @@ export const getBarChartAxisConfigs = ({
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
valueTickValues,
|
||||
tickConfig,
|
||||
tickConfiguration,
|
||||
margins,
|
||||
}: GetBarChartAxisConfigsProps) => {
|
||||
const {
|
||||
categoryTickValues,
|
||||
@@ -33,25 +35,24 @@ export const getBarChartAxisConfigs = ({
|
||||
maxBottomAxisTickLabelLength,
|
||||
maxLeftAxisTickLabelLength,
|
||||
bottomAxisTickRotation,
|
||||
} = tickConfig;
|
||||
} = tickConfiguration;
|
||||
|
||||
const resolvedValueTickValues =
|
||||
valueTickValues && valueTickValues.length > 0
|
||||
? valueTickValues
|
||||
: numberOfValueTicks;
|
||||
|
||||
const baseMargins = getBarChartMargins({ xAxisLabel, yAxisLabel });
|
||||
|
||||
const hasRotation = bottomAxisTickRotation !== 0;
|
||||
const margins =
|
||||
layout === BarChartLayout.VERTICAL && hasRotation
|
||||
? {
|
||||
...baseMargins,
|
||||
bottom:
|
||||
baseMargins.bottom +
|
||||
COMMON_CHART_CONSTANTS.ROTATED_LABELS_EXTRA_BOTTOM_MARGIN,
|
||||
}
|
||||
: baseMargins;
|
||||
const baseBottomLegendOffset =
|
||||
BAR_CHART_CONSTANTS.BOTTOM_AXIS_LEGEND_OFFSET +
|
||||
(hasRotation ? BAR_CHART_CONSTANTS.ROTATED_LABELS_EXTRA_BOTTOM_MARGIN : 0);
|
||||
const bottomLegendOffset = Math.min(
|
||||
baseBottomLegendOffset,
|
||||
Math.max(
|
||||
margins.bottom - COMMON_CHART_CONSTANTS.LEGEND_OFFSET_MARGIN_BUFFER,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
if (layout === BarChartLayout.VERTICAL) {
|
||||
return {
|
||||
@@ -62,11 +63,7 @@ export const getBarChartAxisConfigs = ({
|
||||
tickValues: categoryTickValues,
|
||||
tickRotation: bottomAxisTickRotation,
|
||||
legend: xAxisLabel,
|
||||
legendOffset:
|
||||
BAR_CHART_CONSTANTS.BOTTOM_AXIS_LEGEND_OFFSET +
|
||||
(hasRotation
|
||||
? BAR_CHART_CONSTANTS.ROTATED_LABELS_EXTRA_BOTTOM_MARGIN
|
||||
: 0),
|
||||
legendOffset: bottomLegendOffset,
|
||||
format: (value: string | number) =>
|
||||
truncateTickLabel(String(value), maxBottomAxisTickLabelLength),
|
||||
},
|
||||
@@ -97,7 +94,13 @@ export const getBarChartAxisConfigs = ({
|
||||
tickRotation: BAR_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
tickValues: resolvedValueTickValues,
|
||||
legend: xAxisLabel,
|
||||
legendOffset: BAR_CHART_CONSTANTS.BOTTOM_AXIS_LEGEND_OFFSET,
|
||||
legendOffset: Math.min(
|
||||
BAR_CHART_CONSTANTS.BOTTOM_AXIS_LEGEND_OFFSET,
|
||||
Math.max(
|
||||
margins.bottom - COMMON_CHART_CONSTANTS.LEGEND_OFFSET_MARGIN_BUFFER,
|
||||
0,
|
||||
),
|
||||
),
|
||||
format: (value: number) =>
|
||||
truncateTickLabel(
|
||||
formatGraphValue(value, formatOptions ?? {}),
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
|
||||
import { getBarChartAxisConfigs } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartAxisConfigs';
|
||||
import {
|
||||
getBarChartTickConfig,
|
||||
type BarChartTickConfig,
|
||||
} from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getBarChartTickConfig';
|
||||
import { truncateTickLabel } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/truncateTickLabel';
|
||||
import { type ChartAxisTheme } from '@/page-layout/widgets/graph/types/ChartAxisTheme';
|
||||
import { type ChartMargins } from '@/page-layout/widgets/graph/types/ChartMargins';
|
||||
import { computeBottomLegendOffsetFromText } from '@/page-layout/widgets/graph/utils/computeBottomLegendOffsetFromText';
|
||||
import { computeChartMargins } from '@/page-layout/widgets/graph/utils/computeChartMargins';
|
||||
import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues';
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
import { resolveAxisFontSizes } from '@/page-layout/widgets/graph/utils/resolveAxisFontSizes';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
import { BarChartLayout } from '~/generated/graphql';
|
||||
|
||||
type GetBarChartLayoutParams = {
|
||||
axisTheme: ChartAxisTheme;
|
||||
chartWidth: number;
|
||||
chartHeight: number;
|
||||
data: BarDatum[];
|
||||
indexBy: string;
|
||||
layout: BarChartLayout;
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
formatOptions: GraphValueFormatOptions;
|
||||
effectiveMinimumValue: number;
|
||||
effectiveMaximumValue: number;
|
||||
};
|
||||
|
||||
type BarChartLayoutResult = {
|
||||
margins: ChartMargins;
|
||||
axisBottomConfiguration: ReturnType<
|
||||
typeof getBarChartAxisConfigs
|
||||
>['axisBottom'];
|
||||
axisLeftConfiguration: ReturnType<typeof getBarChartAxisConfigs>['axisLeft'];
|
||||
valueTickValues: number[];
|
||||
valueDomain: { min: number; max: number };
|
||||
};
|
||||
|
||||
const resolveMarginInputs = ({
|
||||
tickConfiguration,
|
||||
tickResult,
|
||||
layout,
|
||||
formatOptions,
|
||||
}: {
|
||||
tickConfiguration: BarChartTickConfig;
|
||||
tickResult: ReturnType<typeof computeValueTickValues>;
|
||||
layout: BarChartLayout;
|
||||
formatOptions: GraphValueFormatOptions;
|
||||
}) => {
|
||||
const formatCategoryTick = (value: string | number, maxLength: number) =>
|
||||
truncateTickLabel(String(value), maxLength);
|
||||
const formatValueTick = (value: number, maxLength: number) =>
|
||||
truncateTickLabel(formatGraphValue(value, formatOptions), maxLength);
|
||||
|
||||
const bottomTickLabels =
|
||||
layout === BarChartLayout.VERTICAL
|
||||
? tickConfiguration.categoryTickValues.map((value) =>
|
||||
formatCategoryTick(
|
||||
value,
|
||||
tickConfiguration.maxBottomAxisTickLabelLength,
|
||||
),
|
||||
)
|
||||
: tickResult.tickValues.map((value) =>
|
||||
formatValueTick(
|
||||
value,
|
||||
tickConfiguration.maxBottomAxisTickLabelLength,
|
||||
),
|
||||
);
|
||||
|
||||
const leftTickLabels =
|
||||
layout === BarChartLayout.VERTICAL
|
||||
? tickResult.tickValues.map((value) =>
|
||||
formatGraphValue(value, formatOptions),
|
||||
)
|
||||
: tickConfiguration.categoryTickValues.map((value) => String(value));
|
||||
|
||||
return { bottomTickLabels, leftTickLabels };
|
||||
};
|
||||
|
||||
const resolveAxisBottomConfiguration = ({
|
||||
axisBottomConfigurationBase,
|
||||
margins,
|
||||
bottomLegendOffset,
|
||||
}: {
|
||||
axisBottomConfigurationBase: ReturnType<
|
||||
typeof getBarChartAxisConfigs
|
||||
>['axisBottom'];
|
||||
margins: ChartMargins;
|
||||
bottomLegendOffset?: number;
|
||||
}) => {
|
||||
const resolvedLegendOffset =
|
||||
bottomLegendOffset ?? axisBottomConfigurationBase.legendOffset;
|
||||
|
||||
return {
|
||||
...axisBottomConfigurationBase,
|
||||
legendOffset: Math.min(
|
||||
resolvedLegendOffset,
|
||||
Math.max(
|
||||
margins.bottom - COMMON_CHART_CONSTANTS.LEGEND_OFFSET_MARGIN_BUFFER,
|
||||
0,
|
||||
),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const getBarChartLayout = ({
|
||||
axisTheme,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
data,
|
||||
indexBy,
|
||||
layout,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
effectiveMinimumValue,
|
||||
effectiveMaximumValue,
|
||||
}: GetBarChartLayoutParams): BarChartLayoutResult => {
|
||||
const { tickFontSize, legendFontSize } = resolveAxisFontSizes(axisTheme);
|
||||
|
||||
const { margins, tickConfiguration, valueTickResult, bottomLegendOffset } =
|
||||
computeChartMargins({
|
||||
tickFontSize,
|
||||
legendFontSize,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
initialTickRotation: BAR_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
computeTickConfig: (currentMargins) =>
|
||||
getBarChartTickConfig({
|
||||
width: chartWidth,
|
||||
height: chartHeight,
|
||||
data,
|
||||
indexBy,
|
||||
axisFontSize: tickFontSize,
|
||||
layout,
|
||||
margins: currentMargins,
|
||||
}),
|
||||
computeValueTickValues: (currentTickConfiguration) =>
|
||||
computeValueTickValues({
|
||||
minimum: effectiveMinimumValue,
|
||||
maximum: effectiveMaximumValue,
|
||||
tickCount: currentTickConfiguration.numberOfValueTicks,
|
||||
}),
|
||||
getTickRotation: (currentTickConfiguration) =>
|
||||
currentTickConfiguration.bottomAxisTickRotation,
|
||||
getBottomLegendOffset: (parameters) =>
|
||||
computeBottomLegendOffsetFromText({
|
||||
tickLabels: parameters.marginInputs.bottomTickLabels,
|
||||
tickFontSize,
|
||||
tickRotation: parameters.tickConfiguration.bottomAxisTickRotation,
|
||||
}),
|
||||
resolveMarginInputs: (currentTickConfiguration, tickResult) =>
|
||||
resolveMarginInputs({
|
||||
tickConfiguration: currentTickConfiguration,
|
||||
tickResult,
|
||||
layout,
|
||||
formatOptions,
|
||||
}),
|
||||
});
|
||||
|
||||
const { tickValues: valueTickValues, domain: valueDomain } = valueTickResult;
|
||||
|
||||
const {
|
||||
axisBottom: axisBottomConfigurationBase,
|
||||
axisLeft: axisLeftConfiguration,
|
||||
} = getBarChartAxisConfigs({
|
||||
layout,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
valueTickValues,
|
||||
tickConfiguration,
|
||||
margins,
|
||||
});
|
||||
|
||||
const axisBottomConfiguration = resolveAxisBottomConfiguration({
|
||||
axisBottomConfigurationBase,
|
||||
margins,
|
||||
bottomLegendOffset,
|
||||
});
|
||||
|
||||
return {
|
||||
margins,
|
||||
axisBottomConfiguration,
|
||||
axisLeftConfiguration,
|
||||
valueTickValues,
|
||||
valueDomain,
|
||||
};
|
||||
};
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const BAR_CHART_MARGINS = {
|
||||
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: 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: 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: 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 = ({
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
}: {
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
}) => {
|
||||
if (isDefined(xAxisLabel) && isDefined(yAxisLabel)) {
|
||||
return BAR_CHART_MARGINS_WITH_BOTH_LABELS;
|
||||
}
|
||||
|
||||
if (isDefined(xAxisLabel)) {
|
||||
return BAR_CHART_MARGINS_WITH_X_LABEL;
|
||||
}
|
||||
|
||||
if (isDefined(yAxisLabel)) {
|
||||
return BAR_CHART_MARGINS_WITH_Y_LABEL;
|
||||
}
|
||||
|
||||
return BAR_CHART_MARGINS;
|
||||
};
|
||||
+13
-25
@@ -1,8 +1,7 @@
|
||||
import { BAR_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetBarChart/constants/BarChartConstants';
|
||||
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 { type ChartMargins } from '@/page-layout/widgets/graph/types/ChartMargins';
|
||||
import { computeMaxLabelLengthForMargin } from '@/page-layout/widgets/graph/utils/computeMaxLabelLengthForMargin';
|
||||
import { getTickRotationConfig } from '@/page-layout/widgets/graph/utils/getTickRotationConfig';
|
||||
import { type BarDatum } from '@nivo/bar';
|
||||
@@ -21,19 +20,17 @@ export const getBarChartTickConfig = ({
|
||||
height,
|
||||
data,
|
||||
indexBy,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
axisFontSize,
|
||||
layout,
|
||||
margins,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
data: BarDatum[];
|
||||
indexBy: string;
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
axisFontSize: number;
|
||||
layout: BarChartLayout;
|
||||
margins: ChartMargins;
|
||||
}): BarChartTickConfig => {
|
||||
const clampValueTickCount = (tickCount: number) =>
|
||||
Math.min(
|
||||
@@ -46,13 +43,10 @@ export const getBarChartTickConfig = ({
|
||||
axisFontSize,
|
||||
data,
|
||||
indexBy,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
margins,
|
||||
layout,
|
||||
});
|
||||
|
||||
const margins = getBarChartMargins({ xAxisLabel, yAxisLabel });
|
||||
|
||||
const availableWidth = width - (margins.left + margins.right);
|
||||
const availableHeight = height - (margins.top + margins.bottom);
|
||||
|
||||
@@ -65,23 +59,17 @@ export const getBarChartTickConfig = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const widthPerTick = calculateWidthPerTick({
|
||||
layout,
|
||||
availableWidth,
|
||||
categoryTickCount: categoryTickValues.length,
|
||||
valueTickCount: numberOfValueTicks,
|
||||
});
|
||||
|
||||
const actualDataPointCount = data.length;
|
||||
const widthPerDataPoint =
|
||||
layout === BarChartLayout.VERTICAL &&
|
||||
actualDataPointCount > 0 &&
|
||||
availableWidth > 0
|
||||
? availableWidth / actualDataPointCount
|
||||
: widthPerTick;
|
||||
const bottomTickCount =
|
||||
layout === BarChartLayout.VERTICAL
|
||||
? categoryTickValues.length
|
||||
: numberOfValueTicks;
|
||||
const widthPerBottomTick =
|
||||
bottomTickCount > 0 && availableWidth > 0
|
||||
? availableWidth / bottomTickCount
|
||||
: 0;
|
||||
|
||||
const tickRotationConfig = getTickRotationConfig({
|
||||
widthPerTick: widthPerDataPoint,
|
||||
widthPerTick: widthPerBottomTick,
|
||||
axisFontSize,
|
||||
});
|
||||
|
||||
|
||||
+28
-35
@@ -1,7 +1,6 @@
|
||||
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,
|
||||
@@ -10,18 +9,15 @@ 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_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';
|
||||
import { graphWidgetLineTooltipComponentState } from '@/page-layout/widgets/graph/graphWidgetLineChart/states/graphWidgetLineTooltipComponentState';
|
||||
import { type LineChartSeriesWithColor } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeriesWithColor';
|
||||
import { calculateValueRangeFromLineChartSeries } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/calculateValueRangeFromLineChartSeries';
|
||||
import { getLineChartAxisBottomConfig } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisBottomConfig';
|
||||
import { getLineChartAxisLeftConfig } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisLeftConfig';
|
||||
import { getLineChartLayout } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartLayout';
|
||||
import { type GraphColorMode } from '@/page-layout/widgets/graph/types/GraphColorMode';
|
||||
import { computeEffectiveValueRange } from '@/page-layout/widgets/graph/utils/computeEffectiveValueRange';
|
||||
import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues';
|
||||
import { createGraphColorRegistry } from '@/page-layout/widgets/graph/utils/createGraphColorRegistry';
|
||||
import {
|
||||
formatGraphValue,
|
||||
@@ -164,6 +160,23 @@ export const GraphWidgetLineChart = ({
|
||||
debouncedHideTooltip();
|
||||
};
|
||||
|
||||
const {
|
||||
margins,
|
||||
axisBottomConfiguration,
|
||||
axisLeftConfiguration,
|
||||
valueTickValues,
|
||||
valueDomain,
|
||||
} = getLineChartLayout({
|
||||
axisTheme: chartTheme.axis,
|
||||
chartWidth,
|
||||
data,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
effectiveMinimumValue,
|
||||
effectiveMaximumValue,
|
||||
});
|
||||
|
||||
const handleSliceEnter = (sliceData: SliceHoverData) => {
|
||||
const slice: SliceTooltipProps<LineSeries>['slice'] = {
|
||||
id: String(sliceData.nearestSlice.xValue ?? ''),
|
||||
@@ -176,8 +189,8 @@ export const GraphWidgetLineChart = ({
|
||||
points: sliceData.nearestSlice.points,
|
||||
};
|
||||
|
||||
const offsetLeft = sliceData.nearestSlice.x + marginLeft;
|
||||
const offsetTop = sliceData.mouseY + COMMON_CHART_CONSTANTS.MARGIN_TOP;
|
||||
const offsetLeft = sliceData.nearestSlice.x + margins.left;
|
||||
const offsetTop = sliceData.mouseY + margins.top;
|
||||
|
||||
debouncedHideTooltip.cancel();
|
||||
setCrosshairX(sliceData.sliceX);
|
||||
@@ -217,8 +230,8 @@ export const GraphWidgetLineChart = ({
|
||||
points={layerProps.points}
|
||||
innerHeight={layerProps.innerHeight}
|
||||
innerWidth={layerProps.innerWidth}
|
||||
marginLeft={marginLeft}
|
||||
marginTop={COMMON_CHART_CONSTANTS.MARGIN_TOP}
|
||||
marginLeft={margins.left}
|
||||
marginTop={margins.top}
|
||||
onSliceHover={handleSliceEnter}
|
||||
onSliceClick={
|
||||
isDefined(onSliceClick)
|
||||
@@ -269,26 +282,6 @@ export const GraphWidgetLineChart = ({
|
||||
/>
|
||||
);
|
||||
|
||||
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_CONSTANTS.DEFAULT_TICK_COUNT,
|
||||
});
|
||||
|
||||
const axisLeftConfig = getLineChartAxisLeftConfig(
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
valueTickValues,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer id={id}>
|
||||
<GraphWidgetChartContainer
|
||||
@@ -303,10 +296,10 @@ export const GraphWidgetLineChart = ({
|
||||
<ResponsiveLine
|
||||
data={nivoData}
|
||||
margin={{
|
||||
top: COMMON_CHART_CONSTANTS.MARGIN_TOP,
|
||||
right: COMMON_CHART_CONSTANTS.MARGIN_RIGHT,
|
||||
bottom: marginBottom,
|
||||
left: marginLeft,
|
||||
top: margins.top,
|
||||
right: margins.right,
|
||||
bottom: margins.bottom,
|
||||
left: margins.left,
|
||||
}}
|
||||
xScale={{ type: 'point' }}
|
||||
yScale={{
|
||||
@@ -325,8 +318,8 @@ export const GraphWidgetLineChart = ({
|
||||
colors={colors}
|
||||
axisTop={null}
|
||||
axisRight={null}
|
||||
axisBottom={axisBottomConfig}
|
||||
axisLeft={axisLeftConfig}
|
||||
axisBottom={axisBottomConfiguration}
|
||||
axisLeft={axisLeftConfiguration}
|
||||
enableGridX={showGrid}
|
||||
enableGridY={showGrid}
|
||||
gridYValues={valueTickValues}
|
||||
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
import { parseFontSizeToPx } from '@/page-layout/widgets/graph/utils/parseFontSizeToPx';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
export const useLineChartTheme = () => {
|
||||
const theme = useTheme();
|
||||
const tickFontSize = 12;
|
||||
const legendFontSize = parseFontSizeToPx(theme.font.size.sm, tickFontSize);
|
||||
|
||||
return {
|
||||
axis: {
|
||||
@@ -18,13 +21,13 @@ export const useLineChartTheme = () => {
|
||||
},
|
||||
text: {
|
||||
fill: theme.font.color.secondary,
|
||||
fontSize: 12,
|
||||
fontSize: tickFontSize,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
text: {
|
||||
fill: theme.font.color.primary,
|
||||
fontSize: theme.font.size.sm,
|
||||
fontSize: legendFontSize,
|
||||
fontWeight: theme.font.weight.medium,
|
||||
},
|
||||
},
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { TEXT_MARGIN_LIMITS } from '@/page-layout/widgets/graph/constants/TextMarginLimits';
|
||||
import { type LineChartSeriesWithColor } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeriesWithColor';
|
||||
import { getLineChartLayout } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartLayout';
|
||||
import { type ChartAxisTheme } from '@/page-layout/widgets/graph/types/ChartAxisTheme';
|
||||
import { type GraphValueFormatOptions } from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
|
||||
describe('getLineChartLayout', () => {
|
||||
const defaultAxisTheme: ChartAxisTheme = {
|
||||
ticks: { text: { fontSize: 11 } },
|
||||
legend: { text: { fontSize: 11 } },
|
||||
};
|
||||
|
||||
const defaultFormatOptions: GraphValueFormatOptions = {
|
||||
displayType: 'number',
|
||||
};
|
||||
|
||||
const defaultData: LineChartSeriesWithColor[] = [
|
||||
{
|
||||
id: 'series1',
|
||||
label: 'Series 1',
|
||||
data: [
|
||||
{ x: '2024-01', y: 10 },
|
||||
{ x: '2024-02', y: 20 },
|
||||
{ x: '2024-03', y: 30 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const baseParams = {
|
||||
axisTheme: defaultAxisTheme,
|
||||
chartWidth: 500,
|
||||
data: defaultData,
|
||||
xAxisLabel: 'Date',
|
||||
yAxisLabel: 'Value',
|
||||
formatOptions: defaultFormatOptions,
|
||||
effectiveMinimumValue: 0,
|
||||
effectiveMaximumValue: 30,
|
||||
};
|
||||
|
||||
it('produces correct axis configurations', () => {
|
||||
const result = getLineChartLayout(baseParams);
|
||||
|
||||
expect(result.axisBottomConfiguration).toBeDefined();
|
||||
expect(result.axisLeftConfiguration).toBeDefined();
|
||||
expect(result.valueTickValues).toBeDefined();
|
||||
expect(result.valueDomain).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns margins within TEXT_MARGIN_LIMITS bounds', () => {
|
||||
const result = getLineChartLayout(baseParams);
|
||||
|
||||
expect(result.margins.top).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.top,
|
||||
);
|
||||
expect(result.margins.top).toBeLessThanOrEqual(TEXT_MARGIN_LIMITS.max.top);
|
||||
expect(result.margins.right).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.right,
|
||||
);
|
||||
expect(result.margins.right).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.right,
|
||||
);
|
||||
expect(result.margins.bottom).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.bottom,
|
||||
);
|
||||
expect(result.margins.bottom).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.bottom,
|
||||
);
|
||||
expect(result.margins.left).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.left,
|
||||
);
|
||||
expect(result.margins.left).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.left,
|
||||
);
|
||||
});
|
||||
|
||||
it('handles empty data array', () => {
|
||||
const result = getLineChartLayout({
|
||||
...baseParams,
|
||||
data: [],
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.axisBottomConfiguration).toBeDefined();
|
||||
expect(result.axisLeftConfiguration).toBeDefined();
|
||||
});
|
||||
|
||||
it('handles single data point series', () => {
|
||||
const result = getLineChartLayout({
|
||||
...baseParams,
|
||||
data: [
|
||||
{
|
||||
id: 'series1',
|
||||
label: 'Series 1',
|
||||
data: [{ x: '2024-01', y: 50 }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.valueTickValues.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('axisLeftConfiguration.legendOffset is correct relative to margins.left', () => {
|
||||
const result = getLineChartLayout(baseParams);
|
||||
|
||||
const legendOffset = result.axisLeftConfiguration.legendOffset;
|
||||
|
||||
if (legendOffset !== undefined) {
|
||||
expect(Math.abs(legendOffset)).toBeLessThanOrEqual(
|
||||
result.margins.left -
|
||||
COMMON_CHART_CONSTANTS.LEFT_AXIS_LEGEND_OFFSET_PADDING,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('handles multiple series', () => {
|
||||
const result = getLineChartLayout({
|
||||
...baseParams,
|
||||
data: [
|
||||
{
|
||||
id: 'series1',
|
||||
label: 'Series 1',
|
||||
data: [
|
||||
{ x: '2024-01', y: 10 },
|
||||
{ x: '2024-02', y: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'series2',
|
||||
label: 'Series 2',
|
||||
data: [
|
||||
{ x: '2024-01', y: 15 },
|
||||
{ x: '2024-02', y: 25 },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.axisBottomConfiguration).toBeDefined();
|
||||
});
|
||||
|
||||
it('handles missing axis labels', () => {
|
||||
const result = getLineChartLayout({
|
||||
...baseParams,
|
||||
xAxisLabel: undefined,
|
||||
yAxisLabel: undefined,
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.axisBottomConfiguration).toBeDefined();
|
||||
expect(result.axisLeftConfiguration).toBeDefined();
|
||||
});
|
||||
|
||||
it('handles large value ranges with negative and positive values', () => {
|
||||
const result = getLineChartLayout({
|
||||
...baseParams,
|
||||
effectiveMinimumValue: -500,
|
||||
effectiveMaximumValue: 1000,
|
||||
});
|
||||
|
||||
expect(result.valueDomain.min).toBeLessThanOrEqual(-500);
|
||||
expect(result.valueDomain.max).toBeGreaterThanOrEqual(1000);
|
||||
expect(result.valueTickValues.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
+26
-33
@@ -4,7 +4,6 @@ import { type LineChartSeriesWithColor } from '@/page-layout/widgets/graph/graph
|
||||
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: {
|
||||
@@ -17,55 +16,50 @@ export type LineChartAxisBottomResult = {
|
||||
legendOffset: number;
|
||||
format: (value: string | number) => string;
|
||||
};
|
||||
marginBottom: number;
|
||||
};
|
||||
|
||||
export const getLineChartAxisBottomConfig = (
|
||||
xAxisLabel?: string,
|
||||
width?: number,
|
||||
data?: LineChartSeriesWithColor[],
|
||||
marginLeft?: number,
|
||||
): LineChartAxisBottomResult => {
|
||||
const effectiveMarginLeft =
|
||||
marginLeft ?? COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITH_LABEL;
|
||||
type LineChartAxisBottomConfigParams = {
|
||||
xAxisLabel?: string;
|
||||
width: number;
|
||||
data: LineChartSeriesWithColor[];
|
||||
marginLeft: number;
|
||||
marginRight: number;
|
||||
axisFontSize: number;
|
||||
};
|
||||
|
||||
export const getLineChartAxisBottomConfig = ({
|
||||
xAxisLabel,
|
||||
width,
|
||||
data,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
axisFontSize,
|
||||
}: LineChartAxisBottomConfigParams): LineChartAxisBottomResult => {
|
||||
const tickValues =
|
||||
width && data
|
||||
width > 0 && data.length > 0
|
||||
? computeLineChartCategoryTickValues({
|
||||
width,
|
||||
data,
|
||||
marginLeft: effectiveMarginLeft,
|
||||
marginRight: COMMON_CHART_CONSTANTS.MARGIN_RIGHT,
|
||||
marginLeft,
|
||||
marginRight,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const availableWidth = width
|
||||
? width - (effectiveMarginLeft + COMMON_CHART_CONSTANTS.MARGIN_RIGHT)
|
||||
: 0;
|
||||
const availableWidth = width - (marginLeft + marginRight);
|
||||
|
||||
const actualDataPointCount = isNonEmptyArray(data?.[0]?.data)
|
||||
? data[0].data.length
|
||||
: 1;
|
||||
|
||||
const widthPerDataPoint =
|
||||
actualDataPointCount > 0 && availableWidth > 0
|
||||
? availableWidth / actualDataPointCount
|
||||
: 0;
|
||||
const tickCount = tickValues?.length ?? actualDataPointCount;
|
||||
const widthPerTick =
|
||||
tickCount > 0 && availableWidth > 0 ? availableWidth / tickCount : 0;
|
||||
|
||||
const { tickRotation, maxLabelLength } = getTickRotationConfig({
|
||||
widthPerTick: widthPerDataPoint,
|
||||
axisFontSize: COMMON_CHART_CONSTANTS.AXIS_FONT_SIZE,
|
||||
widthPerTick,
|
||||
axisFontSize,
|
||||
});
|
||||
|
||||
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 {
|
||||
config: {
|
||||
tickSize: COMMON_CHART_CONSTANTS.TICK_SIZE,
|
||||
@@ -76,12 +70,11 @@ export const getLineChartAxisBottomConfig = (
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset:
|
||||
COMMON_CHART_CONSTANTS.BOTTOM_AXIS_LEGEND_OFFSET +
|
||||
(hasRotation
|
||||
(tickRotation !== 0
|
||||
? COMMON_CHART_CONSTANTS.ROTATED_LABELS_EXTRA_BOTTOM_MARGIN
|
||||
: 0),
|
||||
format: (value: string | number) =>
|
||||
truncateTickLabel(String(value), maxLabelLength),
|
||||
},
|
||||
marginBottom,
|
||||
};
|
||||
};
|
||||
|
||||
+13
-14
@@ -3,20 +3,20 @@ import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getLineChartAxisLeftConfig = (
|
||||
yAxisLabel?: string,
|
||||
formatOptions?: GraphValueFormatOptions,
|
||||
tickValues?: number[],
|
||||
marginLeft?: number,
|
||||
) => {
|
||||
const effectiveMarginLeft =
|
||||
marginLeft ??
|
||||
(isDefined(yAxisLabel)
|
||||
? COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITH_LABEL
|
||||
: COMMON_CHART_CONSTANTS.MARGIN_LEFT_WITHOUT_LABEL);
|
||||
type LineChartAxisLeftConfigParams = {
|
||||
yAxisLabel?: string;
|
||||
formatOptions?: GraphValueFormatOptions;
|
||||
tickValues?: number[];
|
||||
marginLeft: number;
|
||||
};
|
||||
|
||||
export const getLineChartAxisLeftConfig = ({
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
tickValues,
|
||||
marginLeft,
|
||||
}: LineChartAxisLeftConfigParams) => {
|
||||
return {
|
||||
tickSize: COMMON_CHART_CONSTANTS.TICK_SIZE,
|
||||
tickPadding: COMMON_CHART_CONSTANTS.TICK_PADDING,
|
||||
@@ -25,8 +25,7 @@ export const getLineChartAxisLeftConfig = (
|
||||
legend: yAxisLabel,
|
||||
legendPosition: 'middle' as const,
|
||||
legendOffset:
|
||||
-effectiveMarginLeft +
|
||||
COMMON_CHART_CONSTANTS.LEFT_AXIS_LEGEND_OFFSET_PADDING,
|
||||
-marginLeft + COMMON_CHART_CONSTANTS.LEFT_AXIS_LEGEND_OFFSET_PADDING,
|
||||
format: (value: number) => formatGraphValue(value, formatOptions || {}),
|
||||
};
|
||||
};
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { LINE_CHART_CONSTANTS } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartConstants';
|
||||
import { type LineChartSeriesWithColor } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartSeriesWithColor';
|
||||
import { getLineChartAxisBottomConfig } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisBottomConfig';
|
||||
import { getLineChartAxisLeftConfig } from '@/page-layout/widgets/graph/graphWidgetLineChart/utils/getLineChartAxisLeftConfig';
|
||||
import { type ChartAxisTheme } from '@/page-layout/widgets/graph/types/ChartAxisTheme';
|
||||
import { type ChartMargins } from '@/page-layout/widgets/graph/types/ChartMargins';
|
||||
import { computeBottomLegendOffsetFromText } from '@/page-layout/widgets/graph/utils/computeBottomLegendOffsetFromText';
|
||||
import { computeChartMargins } from '@/page-layout/widgets/graph/utils/computeChartMargins';
|
||||
import { computeValueTickValues } from '@/page-layout/widgets/graph/utils/computeValueTickValues';
|
||||
import {
|
||||
formatGraphValue,
|
||||
type GraphValueFormatOptions,
|
||||
} from '@/page-layout/widgets/graph/utils/graphFormatters';
|
||||
import { resolveAxisFontSizes } from '@/page-layout/widgets/graph/utils/resolveAxisFontSizes';
|
||||
|
||||
type GetLineChartLayoutParams = {
|
||||
axisTheme: ChartAxisTheme;
|
||||
chartWidth: number;
|
||||
data: LineChartSeriesWithColor[];
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
formatOptions: GraphValueFormatOptions;
|
||||
effectiveMinimumValue: number;
|
||||
effectiveMaximumValue: number;
|
||||
};
|
||||
|
||||
type LineChartLayout = {
|
||||
margins: ChartMargins;
|
||||
axisBottomConfiguration: ReturnType<
|
||||
typeof getLineChartAxisBottomConfig
|
||||
>['config'];
|
||||
axisLeftConfiguration: ReturnType<typeof getLineChartAxisLeftConfig>;
|
||||
valueTickValues: number[];
|
||||
valueDomain: { min: number; max: number };
|
||||
};
|
||||
|
||||
const resolveAxisBottomConfiguration = ({
|
||||
axisBottomConfigurationResult,
|
||||
margins,
|
||||
bottomLegendOffset,
|
||||
}: {
|
||||
axisBottomConfigurationResult: ReturnType<
|
||||
typeof getLineChartAxisBottomConfig
|
||||
>;
|
||||
margins: ChartMargins;
|
||||
bottomLegendOffset?: number;
|
||||
}) => {
|
||||
const resolvedLegendOffset =
|
||||
bottomLegendOffset ?? axisBottomConfigurationResult.config.legendOffset;
|
||||
|
||||
return {
|
||||
...axisBottomConfigurationResult.config,
|
||||
legendOffset: Math.min(
|
||||
resolvedLegendOffset,
|
||||
Math.max(
|
||||
margins.bottom - COMMON_CHART_CONSTANTS.LEGEND_OFFSET_MARGIN_BUFFER,
|
||||
0,
|
||||
),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const getLineChartLayout = ({
|
||||
axisTheme,
|
||||
chartWidth,
|
||||
data,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
effectiveMinimumValue,
|
||||
effectiveMaximumValue,
|
||||
}: GetLineChartLayoutParams): LineChartLayout => {
|
||||
const { tickFontSize, legendFontSize } = resolveAxisFontSizes(axisTheme);
|
||||
|
||||
const {
|
||||
margins,
|
||||
tickConfiguration: axisBottomConfigurationResult,
|
||||
valueTickResult,
|
||||
bottomLegendOffset,
|
||||
} = computeChartMargins({
|
||||
tickFontSize,
|
||||
legendFontSize,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
initialTickRotation: LINE_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
computeTickConfig: (currentMargins) =>
|
||||
getLineChartAxisBottomConfig({
|
||||
xAxisLabel,
|
||||
width: chartWidth,
|
||||
data,
|
||||
marginLeft: currentMargins.left,
|
||||
marginRight: currentMargins.right,
|
||||
axisFontSize: tickFontSize,
|
||||
}),
|
||||
computeValueTickValues: () =>
|
||||
computeValueTickValues({
|
||||
minimum: effectiveMinimumValue,
|
||||
maximum: effectiveMaximumValue,
|
||||
tickCount: LINE_CHART_CONSTANTS.DEFAULT_TICK_COUNT,
|
||||
}),
|
||||
getTickRotation: (tickConfiguration) =>
|
||||
tickConfiguration.config.tickRotation,
|
||||
getBottomLegendOffset: (parameters) =>
|
||||
computeBottomLegendOffsetFromText({
|
||||
tickLabels: parameters.marginInputs.bottomTickLabels,
|
||||
tickFontSize,
|
||||
tickRotation: parameters.tickConfiguration.config.tickRotation,
|
||||
}),
|
||||
resolveMarginInputs: (tickConfiguration, tickResult) => ({
|
||||
bottomTickLabels: (tickConfiguration.config.tickValues ?? []).map(
|
||||
(value) => tickConfiguration.config.format(value),
|
||||
),
|
||||
leftTickLabels: tickResult.tickValues.map((value) =>
|
||||
formatGraphValue(value, formatOptions),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const axisBottomConfiguration = resolveAxisBottomConfiguration({
|
||||
axisBottomConfigurationResult,
|
||||
margins,
|
||||
bottomLegendOffset,
|
||||
});
|
||||
|
||||
const { tickValues: valueTickValues, domain: valueDomain } = valueTickResult;
|
||||
|
||||
const axisLeftConfiguration = getLineChartAxisLeftConfig({
|
||||
yAxisLabel,
|
||||
formatOptions,
|
||||
tickValues: valueTickValues,
|
||||
marginLeft: margins.left,
|
||||
});
|
||||
|
||||
return {
|
||||
margins,
|
||||
axisBottomConfiguration,
|
||||
axisLeftConfiguration,
|
||||
valueTickValues,
|
||||
valueDomain,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type ChartAxisTheme = {
|
||||
ticks: { text: { fontSize: number } };
|
||||
legend: { text: { fontSize: number } };
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export type ChartMargins = {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { TEXT_MARGIN_EXTRAS } from '@/page-layout/widgets/graph/constants/TextMarginExtras';
|
||||
import { computeBottomLegendOffsetFromText } from '@/page-layout/widgets/graph/utils/computeBottomLegendOffsetFromText';
|
||||
|
||||
describe('computeBottomLegendOffsetFromText', () => {
|
||||
it('uses tick font size when no tick labels are provided', () => {
|
||||
const tickFontSize = 12;
|
||||
const result = computeBottomLegendOffsetFromText({
|
||||
tickLabels: undefined,
|
||||
tickFontSize,
|
||||
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
Math.ceil(
|
||||
tickFontSize +
|
||||
COMMON_CHART_CONSTANTS.TICK_PADDING +
|
||||
TEXT_MARGIN_EXTRAS.tickPaddingExtra +
|
||||
TEXT_MARGIN_EXTRAS.bottomTickExtraNonRotated,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns larger offsets for rotated labels', () => {
|
||||
const tickFontSize = 12;
|
||||
const tickLabels = ['LongLabelValue'];
|
||||
|
||||
const rotated = computeBottomLegendOffsetFromText({
|
||||
tickLabels,
|
||||
tickFontSize,
|
||||
tickRotation: COMMON_CHART_CONSTANTS.TICK_ROTATION_ANGLE,
|
||||
});
|
||||
|
||||
const nonRotated = computeBottomLegendOffsetFromText({
|
||||
tickLabels,
|
||||
tickFontSize,
|
||||
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
});
|
||||
|
||||
expect(rotated).toBeGreaterThan(nonRotated);
|
||||
});
|
||||
|
||||
it('scales rotated offsets with label length', () => {
|
||||
const tickFontSize = 12;
|
||||
const tickRotation = COMMON_CHART_CONSTANTS.TICK_ROTATION_ANGLE;
|
||||
|
||||
const short = computeBottomLegendOffsetFromText({
|
||||
tickLabels: ['AB'],
|
||||
tickFontSize,
|
||||
tickRotation,
|
||||
});
|
||||
const long = computeBottomLegendOffsetFromText({
|
||||
tickLabels: ['AB'.repeat(12)],
|
||||
tickFontSize,
|
||||
tickRotation,
|
||||
});
|
||||
|
||||
expect(long).toBeGreaterThan(short);
|
||||
});
|
||||
});
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { TEXT_MARGIN_LIMITS } from '@/page-layout/widgets/graph/constants/TextMarginLimits';
|
||||
import { type ChartMargins } from '@/page-layout/widgets/graph/types/ChartMargins';
|
||||
import { computeChartMargins } from '@/page-layout/widgets/graph/utils/computeChartMargins';
|
||||
|
||||
describe('computeChartMargins', () => {
|
||||
const defaultTickConfig = {
|
||||
categoryTickValues: ['A', 'B', 'C'],
|
||||
numberOfValueTicks: 5,
|
||||
bottomAxisTickRotation: 0,
|
||||
maxBottomAxisTickLabelLength: 10,
|
||||
};
|
||||
|
||||
const defaultValueTickResult = {
|
||||
tickValues: [0, 25, 50, 75, 100],
|
||||
domain: { min: 0, max: 100 },
|
||||
};
|
||||
|
||||
const baseParams = {
|
||||
tickFontSize: 11,
|
||||
legendFontSize: 11,
|
||||
initialTickRotation: 0,
|
||||
computeTickConfig: () => defaultTickConfig,
|
||||
computeValueTickValues: () => defaultValueTickResult,
|
||||
getTickRotation: () => 0,
|
||||
resolveMarginInputs: () => ({
|
||||
bottomTickLabels: ['A', 'B', 'C'],
|
||||
leftTickLabels: ['0', '25', '50', '75', '100'],
|
||||
}),
|
||||
};
|
||||
|
||||
it('computes margins with simple callbacks', () => {
|
||||
const result = computeChartMargins(baseParams);
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.margins.top).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.top,
|
||||
);
|
||||
expect(result.margins.right).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.right,
|
||||
);
|
||||
expect(result.margins.bottom).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.bottom,
|
||||
);
|
||||
expect(result.margins.left).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.left,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns tick configuration', () => {
|
||||
const result = computeChartMargins(baseParams);
|
||||
|
||||
expect(result.tickConfiguration).toEqual(defaultTickConfig);
|
||||
});
|
||||
|
||||
it('returns value tick result', () => {
|
||||
const result = computeChartMargins(baseParams);
|
||||
|
||||
expect(result.valueTickResult).toEqual(defaultValueTickResult);
|
||||
});
|
||||
|
||||
it('margins stabilize and do not oscillate between iterations', () => {
|
||||
const callHistory: ChartMargins[] = [];
|
||||
|
||||
const result = computeChartMargins({
|
||||
...baseParams,
|
||||
computeTickConfig: (margins) => {
|
||||
callHistory.push({ ...margins });
|
||||
return defaultTickConfig;
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(callHistory.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('works with getBottomLegendOffset callback', () => {
|
||||
const result = computeChartMargins({
|
||||
...baseParams,
|
||||
getBottomLegendOffset: () => 40,
|
||||
});
|
||||
|
||||
expect(result.bottomLegendOffset).toBe(40);
|
||||
expect(result.margins).toBeDefined();
|
||||
});
|
||||
|
||||
it('works without getBottomLegendOffset callback', () => {
|
||||
const result = computeChartMargins(baseParams);
|
||||
|
||||
expect(result.bottomLegendOffset).toBeUndefined();
|
||||
expect(result.margins).toBeDefined();
|
||||
});
|
||||
|
||||
it('handles empty tick label arrays', () => {
|
||||
const result = computeChartMargins({
|
||||
...baseParams,
|
||||
resolveMarginInputs: () => ({
|
||||
bottomTickLabels: [],
|
||||
leftTickLabels: [],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.margins).toBeDefined();
|
||||
expect(result.margins.bottom).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.bottom,
|
||||
);
|
||||
expect(result.margins.left).toBeGreaterThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.min.left,
|
||||
);
|
||||
});
|
||||
|
||||
it('respects margin limits', () => {
|
||||
const result = computeChartMargins(baseParams);
|
||||
|
||||
expect(result.margins.top).toBeLessThanOrEqual(TEXT_MARGIN_LIMITS.max.top);
|
||||
expect(result.margins.right).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.right,
|
||||
);
|
||||
expect(result.margins.bottom).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.bottom,
|
||||
);
|
||||
expect(result.margins.left).toBeLessThanOrEqual(
|
||||
TEXT_MARGIN_LIMITS.max.left,
|
||||
);
|
||||
});
|
||||
});
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { TEXT_MARGIN_EXTRAS } from '@/page-layout/widgets/graph/constants/TextMarginExtras';
|
||||
import { TEXT_MARGIN_LIMITS } from '@/page-layout/widgets/graph/constants/TextMarginLimits';
|
||||
import { getChartMarginsFromText } from '@/page-layout/widgets/graph/utils/getChartMarginsFromText';
|
||||
|
||||
describe('getChartMarginsFromText', () => {
|
||||
it('clamps margins when labels are very large', () => {
|
||||
const longLabel = 'X'.repeat(200);
|
||||
|
||||
const result = getChartMarginsFromText({
|
||||
tickFontSize: 12,
|
||||
legendFontSize: 12,
|
||||
bottomTickLabels: [longLabel],
|
||||
leftTickLabels: [longLabel],
|
||||
xAxisLabel: 'x',
|
||||
yAxisLabel: 'y',
|
||||
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
bottomLegendOffset: 1000,
|
||||
});
|
||||
|
||||
expect(result.bottom).toBe(TEXT_MARGIN_LIMITS.max.bottom);
|
||||
expect(result.left).toBe(TEXT_MARGIN_LIMITS.max.left);
|
||||
expect(result.top).toBe(18);
|
||||
expect(result.right).toBe(18);
|
||||
});
|
||||
|
||||
it('uses bottom legend offset when it exceeds tick and label blocks', () => {
|
||||
const bottomLegendOffset = 100;
|
||||
const result = getChartMarginsFromText({
|
||||
tickFontSize: 10,
|
||||
legendFontSize: 10,
|
||||
bottomTickLabels: ['a'],
|
||||
leftTickLabels: ['b'],
|
||||
xAxisLabel: 'x',
|
||||
yAxisLabel: 'y',
|
||||
tickRotation: COMMON_CHART_CONSTANTS.NO_ROTATION_ANGLE,
|
||||
bottomLegendOffset,
|
||||
});
|
||||
|
||||
const expectedBottom = Math.ceil(
|
||||
bottomLegendOffset + 10 + TEXT_MARGIN_EXTRAS.tickPaddingExtra,
|
||||
);
|
||||
|
||||
expect(result.bottom).toBe(expectedBottom);
|
||||
});
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { getMaxLabelLength } from '@/page-layout/widgets/graph/utils/getMaxLabelLength';
|
||||
|
||||
describe('getMaxLabelLength', () => {
|
||||
it('returns 0 for undefined input', () => {
|
||||
expect(getMaxLabelLength(undefined)).toBe(0);
|
||||
});
|
||||
|
||||
it('returns 0 for empty array', () => {
|
||||
expect(getMaxLabelLength([])).toBe(0);
|
||||
});
|
||||
|
||||
it('returns max length from array of labels', () => {
|
||||
expect(getMaxLabelLength(['a', 'bb', 'ccc'])).toBe(3);
|
||||
});
|
||||
|
||||
it('returns correct length when max is not last', () => {
|
||||
expect(getMaxLabelLength(['longest', 'short', 'med'])).toBe(7);
|
||||
});
|
||||
|
||||
it('handles single item array', () => {
|
||||
expect(getMaxLabelLength(['single'])).toBe(6);
|
||||
});
|
||||
|
||||
it('handles null/undefined items in array', () => {
|
||||
expect(
|
||||
getMaxLabelLength(['valid', null as unknown as string, 'test']),
|
||||
).toBe(5);
|
||||
expect(getMaxLabelLength([undefined as unknown as string, 'abc'])).toBe(3);
|
||||
});
|
||||
|
||||
it('handles empty strings in array', () => {
|
||||
expect(getMaxLabelLength(['', 'abc', ''])).toBe(3);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -82,12 +82,12 @@ describe('getTickRotationConfig', () => {
|
||||
|
||||
it('should handle different font sizes', () => {
|
||||
const smallFontResult: TickRotationConfig = getTickRotationConfig({
|
||||
widthPerTick: 50,
|
||||
widthPerTick: 200,
|
||||
axisFontSize: 8,
|
||||
});
|
||||
|
||||
const largeFontResult: TickRotationConfig = getTickRotationConfig({
|
||||
widthPerTick: 50,
|
||||
widthPerTick: 200,
|
||||
axisFontSize: 16,
|
||||
});
|
||||
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { parseFontSizeToPx } from '@/page-layout/widgets/graph/utils/parseFontSizeToPx';
|
||||
|
||||
describe('parseFontSizeToPx', () => {
|
||||
it('returns numeric font sizes as-is when valid', () => {
|
||||
expect(parseFontSizeToPx(12, 10)).toBe(12);
|
||||
});
|
||||
|
||||
it('falls back on invalid numbers', () => {
|
||||
expect(parseFontSizeToPx(Number.NaN, 10)).toBe(10);
|
||||
expect(parseFontSizeToPx(-1, 10)).toBe(10);
|
||||
});
|
||||
|
||||
it('parses px strings', () => {
|
||||
expect(parseFontSizeToPx('14px', 10)).toBe(14);
|
||||
expect(parseFontSizeToPx('14', 10)).toBe(14);
|
||||
});
|
||||
|
||||
it('parses rem/em using root font size', () => {
|
||||
const computedStyleSpy = jest
|
||||
.spyOn(window, 'getComputedStyle')
|
||||
.mockReturnValue({ fontSize: '20px' } as CSSStyleDeclaration);
|
||||
|
||||
expect(parseFontSizeToPx('1.5rem', 10)).toBe(30);
|
||||
expect(parseFontSizeToPx('2em', 10)).toBe(40);
|
||||
|
||||
computedStyleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('falls back for invalid strings', () => {
|
||||
expect(parseFontSizeToPx('calc(1rem + 2px)', 11)).toBe(11);
|
||||
expect(parseFontSizeToPx('bad', 11)).toBe(11);
|
||||
});
|
||||
|
||||
it('falls back when root font size is invalid', () => {
|
||||
const computedStyleSpy = jest
|
||||
.spyOn(window, 'getComputedStyle')
|
||||
.mockReturnValue({ fontSize: 'oops' } as CSSStyleDeclaration);
|
||||
|
||||
expect(parseFontSizeToPx('1rem', 10)).toBe(
|
||||
COMMON_CHART_CONSTANTS.AXIS_FONT_SIZE,
|
||||
);
|
||||
|
||||
computedStyleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { TEXT_MARGIN_EXTRAS } from '@/page-layout/widgets/graph/constants/TextMarginExtras';
|
||||
import { estimateLineHeight } from '@/page-layout/widgets/graph/utils/estimateLineHeight';
|
||||
import { estimateRotatedHeight } from '@/page-layout/widgets/graph/utils/estimateRotatedHeight';
|
||||
import { getMaxLabelLength } from '@/page-layout/widgets/graph/utils/getMaxLabelLength';
|
||||
|
||||
export const computeBottomLegendOffsetFromText = ({
|
||||
tickLabels,
|
||||
tickFontSize,
|
||||
tickRotation,
|
||||
}: {
|
||||
tickLabels?: string[];
|
||||
tickFontSize: number;
|
||||
tickRotation: number;
|
||||
}) => {
|
||||
if (!tickLabels || tickLabels.length === 0) {
|
||||
const extraSpacing =
|
||||
tickRotation === 0 ? TEXT_MARGIN_EXTRAS.bottomTickExtraNonRotated : 0;
|
||||
return Math.ceil(
|
||||
tickFontSize +
|
||||
COMMON_CHART_CONSTANTS.TICK_PADDING +
|
||||
TEXT_MARGIN_EXTRAS.tickPaddingExtra +
|
||||
extraSpacing,
|
||||
);
|
||||
}
|
||||
|
||||
const effectiveMaxLabelLength = getMaxLabelLength(tickLabels);
|
||||
const bottomTickHeight =
|
||||
tickRotation !== 0
|
||||
? estimateRotatedHeight(
|
||||
effectiveMaxLabelLength,
|
||||
tickFontSize,
|
||||
tickRotation,
|
||||
)
|
||||
: estimateLineHeight(tickFontSize);
|
||||
const effectiveTickHeight =
|
||||
bottomTickHeight > 0 ? bottomTickHeight : tickFontSize;
|
||||
const extraSpacing =
|
||||
tickRotation === 0 ? TEXT_MARGIN_EXTRAS.bottomTickExtraNonRotated : 0;
|
||||
|
||||
return Math.ceil(
|
||||
effectiveTickHeight +
|
||||
COMMON_CHART_CONSTANTS.TICK_PADDING +
|
||||
TEXT_MARGIN_EXTRAS.tickPaddingExtra +
|
||||
extraSpacing,
|
||||
);
|
||||
};
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { type ChartMargins } from '@/page-layout/widgets/graph/types/ChartMargins';
|
||||
import { getChartMarginsFromText } from '@/page-layout/widgets/graph/utils/getChartMarginsFromText';
|
||||
|
||||
type ChartMarginInputs = {
|
||||
bottomTickLabels?: string[];
|
||||
leftTickLabels?: string[];
|
||||
};
|
||||
|
||||
type ComputeChartMarginsParams<TTickConfig, TValueTickResult> = {
|
||||
tickFontSize: number;
|
||||
legendFontSize?: number;
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
initialTickRotation: number;
|
||||
computeTickConfig: (margins: ChartMargins) => TTickConfig;
|
||||
computeValueTickValues: (tickConfiguration: TTickConfig) => TValueTickResult;
|
||||
getTickRotation: (tickConfiguration: TTickConfig) => number;
|
||||
getBottomLegendOffset?: (params: {
|
||||
tickConfiguration: TTickConfig;
|
||||
marginInputs: ChartMarginInputs;
|
||||
}) => number;
|
||||
resolveMarginInputs: (
|
||||
tickConfiguration: TTickConfig,
|
||||
valueTickResult: TValueTickResult,
|
||||
) => ChartMarginInputs;
|
||||
};
|
||||
|
||||
type ComputeChartMarginsResult<TTickConfig, TValueTickResult> = {
|
||||
margins: ChartMargins;
|
||||
tickConfiguration: TTickConfig;
|
||||
valueTickResult: TValueTickResult;
|
||||
bottomLegendOffset?: number;
|
||||
};
|
||||
|
||||
export const computeChartMargins = <TTickConfig, TValueTickResult>({
|
||||
tickFontSize,
|
||||
legendFontSize,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
initialTickRotation,
|
||||
computeTickConfig,
|
||||
computeValueTickValues,
|
||||
getTickRotation,
|
||||
getBottomLegendOffset,
|
||||
resolveMarginInputs,
|
||||
}: ComputeChartMarginsParams<
|
||||
TTickConfig,
|
||||
TValueTickResult
|
||||
>): ComputeChartMarginsResult<TTickConfig, TValueTickResult> => {
|
||||
const provisionalMargins = getChartMarginsFromText({
|
||||
tickFontSize,
|
||||
legendFontSize,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
tickRotation: initialTickRotation,
|
||||
});
|
||||
|
||||
const provisionalTickConfiguration = computeTickConfig(provisionalMargins);
|
||||
const provisionalValueTickResult = computeValueTickValues(
|
||||
provisionalTickConfiguration,
|
||||
);
|
||||
const provisionalMarginInputs = resolveMarginInputs(
|
||||
provisionalTickConfiguration,
|
||||
provisionalValueTickResult,
|
||||
);
|
||||
|
||||
const computedBottomLegendOffset = getBottomLegendOffset
|
||||
? getBottomLegendOffset({
|
||||
tickConfiguration: provisionalTickConfiguration,
|
||||
marginInputs: provisionalMarginInputs,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const computedMargins = getChartMarginsFromText({
|
||||
tickFontSize,
|
||||
legendFontSize,
|
||||
bottomTickLabels: provisionalMarginInputs.bottomTickLabels,
|
||||
leftTickLabels: provisionalMarginInputs.leftTickLabels,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
tickRotation: getTickRotation(provisionalTickConfiguration),
|
||||
bottomLegendOffset: computedBottomLegendOffset,
|
||||
});
|
||||
|
||||
const tickConfiguration = computeTickConfig(computedMargins);
|
||||
const valueTickResult = computeValueTickValues(tickConfiguration);
|
||||
const marginInputs = resolveMarginInputs(tickConfiguration, valueTickResult);
|
||||
|
||||
const bottomLegendOffset = getBottomLegendOffset
|
||||
? getBottomLegendOffset({
|
||||
tickConfiguration,
|
||||
marginInputs,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const margins = getChartMarginsFromText({
|
||||
tickFontSize,
|
||||
legendFontSize,
|
||||
bottomTickLabels: marginInputs.bottomTickLabels,
|
||||
leftTickLabels: marginInputs.leftTickLabels,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
tickRotation: getTickRotation(tickConfiguration),
|
||||
bottomLegendOffset,
|
||||
});
|
||||
|
||||
return { margins, tickConfiguration, valueTickResult, bottomLegendOffset };
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
|
||||
export const estimateLineHeight = (fontSize: number) =>
|
||||
Math.ceil(fontSize * COMMON_CHART_CONSTANTS.TICK_LABEL_LINE_HEIGHT_RATIO);
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { estimateLineHeight } from '@/page-layout/widgets/graph/utils/estimateLineHeight';
|
||||
|
||||
export const estimateRotatedHeight = (
|
||||
maxLength: number,
|
||||
fontSize: number,
|
||||
rotationDegrees: number,
|
||||
) => {
|
||||
if (rotationDegrees === 0 || maxLength <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const labelWidth =
|
||||
maxLength *
|
||||
fontSize *
|
||||
COMMON_CHART_CONSTANTS.ROTATED_TICK_LABEL_WIDTH_ESTIMATE_RATIO;
|
||||
if (labelWidth <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const labelHeight = estimateLineHeight(fontSize);
|
||||
const rotationRadians = (Math.abs(rotationDegrees) * Math.PI) / 180;
|
||||
const projectedHeight =
|
||||
Math.abs(labelWidth * Math.sin(rotationRadians)) +
|
||||
Math.abs(labelHeight * Math.cos(rotationRadians));
|
||||
|
||||
return Math.max(labelHeight, projectedHeight);
|
||||
};
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { TEXT_MARGIN_EXTRAS } from '@/page-layout/widgets/graph/constants/TextMarginExtras';
|
||||
import { TEXT_MARGIN_LIMITS } from '@/page-layout/widgets/graph/constants/TextMarginLimits';
|
||||
import { type ChartMargins } from '@/page-layout/widgets/graph/types/ChartMargins';
|
||||
import { estimateLineHeight } from '@/page-layout/widgets/graph/utils/estimateLineHeight';
|
||||
import { estimateRotatedHeight } from '@/page-layout/widgets/graph/utils/estimateRotatedHeight';
|
||||
import { getMaxLabelLength } from '@/page-layout/widgets/graph/utils/getMaxLabelLength';
|
||||
import { isNumber } from '@sniptt/guards';
|
||||
|
||||
const clamp = (value: number, min: number, max: number) =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
|
||||
const estimateLabelWidth = (maxLength: number, fontSize: number) =>
|
||||
Math.ceil(
|
||||
maxLength *
|
||||
fontSize *
|
||||
COMMON_CHART_CONSTANTS.HORIZONTAL_LABEL_CHARACTER_WIDTH_RATIO,
|
||||
);
|
||||
|
||||
export const getChartMarginsFromText = ({
|
||||
tickFontSize,
|
||||
legendFontSize,
|
||||
bottomTickLabels,
|
||||
leftTickLabels,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
tickRotation,
|
||||
bottomLegendOffset,
|
||||
}: {
|
||||
tickFontSize: number;
|
||||
legendFontSize?: number;
|
||||
bottomTickLabels?: string[];
|
||||
leftTickLabels?: string[];
|
||||
xAxisLabel?: string;
|
||||
yAxisLabel?: string;
|
||||
tickRotation: number;
|
||||
bottomLegendOffset?: number;
|
||||
}): ChartMargins => {
|
||||
const normalizedLegendFontSize = legendFontSize ?? tickFontSize;
|
||||
|
||||
const bottomMaxLabelLength = getMaxLabelLength(bottomTickLabels);
|
||||
const leftMaxLabelLength = getMaxLabelLength(leftTickLabels);
|
||||
|
||||
const tickPaddingExtra = xAxisLabel ? TEXT_MARGIN_EXTRAS.tickPaddingExtra : 0;
|
||||
const bottomTickHeight =
|
||||
bottomMaxLabelLength > 0
|
||||
? tickRotation !== 0
|
||||
? estimateRotatedHeight(
|
||||
bottomMaxLabelLength,
|
||||
tickFontSize,
|
||||
tickRotation,
|
||||
)
|
||||
: estimateLineHeight(tickFontSize)
|
||||
: 0;
|
||||
const bottomTicksBlock =
|
||||
bottomTickHeight > 0
|
||||
? bottomTickHeight +
|
||||
COMMON_CHART_CONSTANTS.TICK_PADDING +
|
||||
tickPaddingExtra
|
||||
: 0;
|
||||
const bottomLabelBlock = xAxisLabel
|
||||
? normalizedLegendFontSize + tickPaddingExtra
|
||||
: 0;
|
||||
const bottomFromTicksAndLabel = bottomTicksBlock + bottomLabelBlock;
|
||||
const bottomFromLegendOffset =
|
||||
xAxisLabel && isNumber(bottomLegendOffset)
|
||||
? bottomLegendOffset + normalizedLegendFontSize + tickPaddingExtra
|
||||
: 0;
|
||||
|
||||
const bottom = clamp(
|
||||
Math.ceil(Math.max(bottomFromTicksAndLabel, bottomFromLegendOffset)),
|
||||
TEXT_MARGIN_LIMITS.min.bottom,
|
||||
TEXT_MARGIN_LIMITS.max.bottom,
|
||||
);
|
||||
|
||||
const leftTickWidth =
|
||||
leftMaxLabelLength > 0
|
||||
? estimateLabelWidth(leftMaxLabelLength, tickFontSize)
|
||||
: 0;
|
||||
const leftTicksBlock =
|
||||
leftTickWidth > 0
|
||||
? leftTickWidth +
|
||||
COMMON_CHART_CONSTANTS.TICK_PADDING +
|
||||
TEXT_MARGIN_EXTRAS.tickPaddingExtra
|
||||
: 0;
|
||||
const leftLabelBlock = yAxisLabel
|
||||
? normalizedLegendFontSize + TEXT_MARGIN_EXTRAS.tickPaddingExtra
|
||||
: 0;
|
||||
const left = clamp(
|
||||
Math.ceil(leftTicksBlock + leftLabelBlock),
|
||||
TEXT_MARGIN_LIMITS.min.left,
|
||||
TEXT_MARGIN_LIMITS.max.left,
|
||||
);
|
||||
|
||||
const topRightBase = Math.ceil(tickFontSize * 1.5);
|
||||
|
||||
return {
|
||||
top: clamp(
|
||||
topRightBase,
|
||||
TEXT_MARGIN_LIMITS.min.top,
|
||||
TEXT_MARGIN_LIMITS.max.top,
|
||||
),
|
||||
right: clamp(
|
||||
topRightBase,
|
||||
TEXT_MARGIN_LIMITS.min.right,
|
||||
TEXT_MARGIN_LIMITS.max.right,
|
||||
),
|
||||
bottom,
|
||||
left,
|
||||
};
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export const getMaxLabelLength = (labels?: string[]) => {
|
||||
if (!labels || labels.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return labels.reduce(
|
||||
(maxLength, label) => Math.max(maxLength, label?.length ?? 0),
|
||||
0,
|
||||
);
|
||||
};
|
||||
+2
-7
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
|
||||
@@ -28,11 +26,8 @@ export const getTickRotationConfig = ({
|
||||
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)),
|
||||
),
|
||||
COMMON_CHART_CONSTANTS.ROTATED_TICK_MINIMUM_NUMBER_OF_DISPLAYED_CHARACTERS,
|
||||
Math.floor(widthPerTick / characterWidth),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { isNumber, isString } from '@sniptt/guards';
|
||||
|
||||
const getRootFontSize = () => {
|
||||
if (typeof document === 'undefined') {
|
||||
return COMMON_CHART_CONSTANTS.AXIS_FONT_SIZE;
|
||||
}
|
||||
|
||||
const rootFontSize = getComputedStyle(document.documentElement).fontSize;
|
||||
const parsed = Number.parseFloat(rootFontSize);
|
||||
|
||||
return Number.isFinite(parsed) && parsed > 0
|
||||
? parsed
|
||||
: COMMON_CHART_CONSTANTS.AXIS_FONT_SIZE;
|
||||
};
|
||||
|
||||
export const parseFontSizeToPx = (
|
||||
fontSize: number | string | undefined,
|
||||
fallback: number,
|
||||
) => {
|
||||
if (isNumber(fontSize)) {
|
||||
return Number.isFinite(fontSize) && fontSize > 0 ? fontSize : fallback;
|
||||
}
|
||||
|
||||
if (isString(fontSize)) {
|
||||
const trimmed = fontSize.trim();
|
||||
const parsed = Number.parseFloat(trimmed);
|
||||
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (trimmed.endsWith('rem') || trimmed.endsWith('em')) {
|
||||
return parsed * getRootFontSize();
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return fallback;
|
||||
};
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { COMMON_CHART_CONSTANTS } from '@/page-layout/widgets/graph/constants/CommonChartConstants';
|
||||
import { type ChartAxisTheme } from '@/page-layout/widgets/graph/types/ChartAxisTheme';
|
||||
|
||||
export const resolveAxisFontSizes = (axisTheme: ChartAxisTheme) => {
|
||||
const tickFontSize =
|
||||
axisTheme?.ticks?.text?.fontSize ?? COMMON_CHART_CONSTANTS.AXIS_FONT_SIZE;
|
||||
const legendFontSize = axisTheme?.legend?.text?.fontSize ?? tickFontSize;
|
||||
|
||||
return { tickFontSize, legendFontSize };
|
||||
};
|
||||
Reference in New Issue
Block a user