diff --git a/apps/web/src/api/asset_interfaces/dashboard/config.ts b/apps/web/src/api/asset_interfaces/dashboard/config.ts deleted file mode 100644 index 52987926d..000000000 --- a/apps/web/src/api/asset_interfaces/dashboard/config.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const MAX_NUMBER_OF_ITEMS_ON_DASHBOARD = 25; -export const NUMBER_OF_COLUMNS = 12; -export const MAX_NUMBER_OF_ITEMS = 4; diff --git a/apps/web/src/api/buster_rest/dashboards/helpers/addMetricToDashboard.ts b/apps/web/src/api/buster_rest/dashboards/helpers/addMetricToDashboard.ts index 26df075f8..c52cfdefe 100644 --- a/apps/web/src/api/buster_rest/dashboards/helpers/addMetricToDashboard.ts +++ b/apps/web/src/api/buster_rest/dashboards/helpers/addMetricToDashboard.ts @@ -1,6 +1,6 @@ +import { MAX_NUMBER_OF_ITEMS, NUMBER_OF_COLUMNS } from '@buster/server-shared/dashboards'; import { v4 as uuidv4 } from 'uuid'; import type { BusterDashboard } from '@/api/asset_interfaces/dashboard'; -import { MAX_NUMBER_OF_ITEMS, NUMBER_OF_COLUMNS } from '../../../asset_interfaces/dashboard/config'; export const addMetricToDashboardConfig = ( metricIds: string[], diff --git a/apps/web/src/api/buster_rest/dashboards/helpers/removeMetricFromDashboard.ts b/apps/web/src/api/buster_rest/dashboards/helpers/removeMetricFromDashboard.ts index ce5a31fe0..ea83cd482 100644 --- a/apps/web/src/api/buster_rest/dashboards/helpers/removeMetricFromDashboard.ts +++ b/apps/web/src/api/buster_rest/dashboards/helpers/removeMetricFromDashboard.ts @@ -1,5 +1,5 @@ +import { NUMBER_OF_COLUMNS } from '@buster/server-shared/dashboards'; import type { BusterDashboard } from '@/api/asset_interfaces/dashboard'; -import { NUMBER_OF_COLUMNS } from '../../../asset_interfaces/dashboard/config'; export const removeMetricFromDashboardConfig = ( metricIds: string[], diff --git a/apps/web/src/api/buster_rest/dashboards/mutations/useMetrics.ts b/apps/web/src/api/buster_rest/dashboards/mutations/useMetrics.ts index 982a43b24..6ee1d6889 100644 --- a/apps/web/src/api/buster_rest/dashboards/mutations/useMetrics.ts +++ b/apps/web/src/api/buster_rest/dashboards/mutations/useMetrics.ts @@ -1,7 +1,7 @@ +import { MAX_NUMBER_OF_ITEMS_ON_DASHBOARD } from '@buster/server-shared/dashboards'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { create } from 'mutative'; import type { BusterDashboardResponse } from '@/api/asset_interfaces/dashboard'; -import { MAX_NUMBER_OF_ITEMS_ON_DASHBOARD } from '@/api/asset_interfaces/dashboard/config'; import { metricsQueryKeys } from '@/api/query_keys/metric'; import { createDashboardFullConfirmModal } from '@/components/features/modals/createDashboardFullConfirmModal'; import { useBusterNotifications } from '@/context/BusterNotifications'; diff --git a/apps/web/src/components/features/modals/createDashboardFullConfirmModal.tsx b/apps/web/src/components/features/modals/createDashboardFullConfirmModal.tsx index f57f202dd..02782bab8 100644 --- a/apps/web/src/components/features/modals/createDashboardFullConfirmModal.tsx +++ b/apps/web/src/components/features/modals/createDashboardFullConfirmModal.tsx @@ -1,4 +1,4 @@ -import { MAX_NUMBER_OF_ITEMS_ON_DASHBOARD } from '@/api/asset_interfaces/dashboard/config'; +import { MAX_NUMBER_OF_ITEMS_ON_DASHBOARD } from '@buster/server-shared/dashboards'; import { cn } from '@/lib/classMerge'; import { Text } from '../../ui/typography/Text'; import { Title } from '../../ui/typography/Title'; diff --git a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useOptions.tsx b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useOptions.tsx index 9e62e6874..4443663df 100644 --- a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useOptions.tsx +++ b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useOptions.tsx @@ -141,6 +141,7 @@ export const useOptions = ({ y2AxisScaleType, y2AxisStartAxisAtZero, columnMetadata, + yAxis, }); const isHorizontalBar = useMemo(() => { diff --git a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.test.ts b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.test.ts index 15a55e780..dabea9bf9 100644 --- a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.test.ts +++ b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.test.ts @@ -22,6 +22,10 @@ describe('useY2Axis', () => { y2AxisScaleType: 'linear' as const, columnMetadata: [], columnSettings: {}, + yAxis: { + min: undefined, + max: undefined, + }, } as Parameters[0]; it('should return undefined display when chart type is not Combo', () => { diff --git a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.ts b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.ts index 1b0a5aa94..c77725573 100644 --- a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.ts +++ b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useY2Axis.ts @@ -7,7 +7,13 @@ import { DEFAULT_CHART_CONFIG, DEFAULT_COLUMN_LABEL_FORMAT, } from '@buster/server-shared/metrics'; -import type { Scale, ScaleChartOptions } from 'chart.js'; +import { fa } from '@faker-js/faker'; +import type { + CartesianScaleTypeRegistry, + Scale, + ScaleChartOptions, + ScaleOptionsByType, +} from 'chart.js'; import { useMemo } from 'react'; import type { DeepPartial } from 'utility-types'; import { useMemoizedFn } from '@/hooks/useMemoizedFn'; @@ -15,7 +21,7 @@ import type { BusterChartProps } from '../../../BusterChart.types'; import { formatYAxisLabel, yAxisSimilar } from '../../../commonHelpers'; import { useY2AxisTitle } from './axisHooks/useY2AxisTitle'; -export const DEFAULT_Y2_AXIS_COUNT = 7; +export const DEFAULT_Y2_AXIS_COUNT = 9; export const useY2Axis = ({ columnLabelFormats, @@ -26,6 +32,7 @@ export const useY2Axis = ({ y2AxisShowAxisLabel, y2AxisStartAxisAtZero, y2AxisScaleType, + yAxis, }: { columnLabelFormats: NonNullable; selectedAxis: ChartEncodes; @@ -36,9 +43,12 @@ export const useY2Axis = ({ y2AxisStartAxisAtZero: BusterChartProps['y2AxisStartAxisAtZero']; y2AxisScaleType: BusterChartProps['y2AxisScaleType']; columnMetadata: NonNullable; + yAxis: DeepPartial | undefined>; }): DeepPartial['scales']['y2']> | undefined => { const selectedAxis = selectedAxisProp as ComboChartAxis; const y2AxisKeys = selectedAxis.y2 || []; + const yAxisMinValue = yAxis?.min; + const yAxisMaxValue = yAxis?.max; const y2AxisKeysString = useMemo(() => { return y2AxisKeys.join(','); @@ -107,6 +117,8 @@ export const useY2Axis = ({ callback: tickCallback, includeBounds: true, }, + min: yAxisMinValue, + max: yAxisMaxValue, grid: { drawOnChartArea: false, // only want the grid lines for one axis to show up }, @@ -116,6 +128,7 @@ export const useY2Axis = ({ }, [ tickCallback, title, + yAxisMinValue, y2AxisKeysString, isSupportedType, y2AxisShowAxisLabel, diff --git a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYAxis.ts b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYAxis.ts index 1ff27894f..8cd844ffa 100644 --- a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYAxis.ts +++ b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYAxis.ts @@ -5,7 +5,6 @@ import { type ColumnLabelFormat, type ComboChartAxis, DEFAULT_COLUMN_LABEL_FORMAT, - DEFAULT_COLUMN_SETTINGS, } from '@buster/server-shared/metrics'; import type { GridLineOptions, Scale, ScaleChartOptions } from 'chart.js'; import { useMemo } from 'react'; @@ -16,6 +15,7 @@ import { formatYAxisLabel, yAxisSimilar } from '../../../commonHelpers'; import { useYAxisTitle } from './axisHooks/useYAxisTitle'; import { useIsStacked } from './useIsStacked'; import { DEFAULT_Y2_AXIS_COUNT } from './useY2Axis'; +import { useYTickValues } from './useYTickValues'; export const useYAxis = ({ columnLabelFormats, @@ -30,7 +30,6 @@ export const useYAxis = ({ yAxisScaleType, gridLines, columnMetadata, - columnSettings, }: { columnLabelFormats: NonNullable; selectedAxis: ChartEncodes; @@ -50,51 +49,23 @@ export const useYAxis = ({ const y2AxisKeys = (selectedAxis as ComboChartAxis)?.y2 || []; const hasY2Axis = y2AxisKeys.length > 0; - const useMinValue = useMemo(() => { - if (!hasY2Axis) return false; - if (selectedChartType !== 'combo') return false; - if (!columnMetadata) return false; - - const checkVales = [...yAxisKeys, ...y2AxisKeys]; - - // Create lookup map for O(1) column access - const columnMap = new Map(columnMetadata.map((col) => [col.name, col])); - - let allBarValues = true; - let hasNegativeValues = false; - - // Single pass to check both conditions - for (const key of checkVales) { - const column = columnMap.get(key); - if (!column) { - allBarValues = false; - continue; - } - - // Check if this column is a bar - const visualization = - columnSettings[column.name]?.columnVisualization || - DEFAULT_COLUMN_SETTINGS.columnVisualization; - if (visualization !== 'bar') { - allBarValues = false; - } - - // Check if this column has negative values - if (Number(column.min_value ?? 0) < 0) { - hasNegativeValues = true; - } - } - - if (allBarValues) return true; - if (hasNegativeValues) return false; - - return false; - }, [hasY2Axis, yAxisKeys, y2AxisKeys, selectedChartType, columnMetadata, columnSettings]); - const isSupportedType = useMemo(() => { return selectedChartType !== 'pie'; }, [selectedChartType]); + const { minTickValue, maxTickValue } = useYTickValues({ + hasY2Axis, + columnMetadata, + selectedChartType, + yAxisKeys, + y2AxisKeys, + columnLabelFormats, + }); + + const defaultTickCount = useMemo(() => { + if (y2AxisKeys.length > 0 && minTickValue !== undefined) return DEFAULT_Y2_AXIS_COUNT; + }, [minTickValue]); + const grid: DeepPartial | undefined = useMemo(() => { return { display: gridLines, @@ -157,11 +128,9 @@ export const useYAxis = ({ const memoizedYAxisOptions: DeepPartial['scales']['y']> | undefined = useMemo(() => { if (!isSupportedType) return undefined; - - const baseConfig = { + return { type, grid, - max: usePercentageModeAxis ? 100 : undefined, beginAtZero: yAxisStartAxisAtZero !== false, stacked, title: { @@ -171,16 +140,15 @@ export const useYAxis = ({ ticks: { display: yAxisShowAxisLabel, callback: tickCallback, - count: useMinValue ? DEFAULT_Y2_AXIS_COUNT : undefined, + count: defaultTickCount, includeBounds: true, }, - min: useMinValue ? 0 : undefined, + min: usePercentageModeAxis ? 0 : minTickValue, + max: usePercentageModeAxis ? 100 : maxTickValue, border: { display: yAxisShowAxisLabel, }, } as DeepPartial['scales']['y']>; - - return baseConfig; }, [ tickCallback, type, @@ -191,6 +159,9 @@ export const useYAxis = ({ yAxisStartAxisAtZero, yAxisShowAxisLabel, usePercentageModeAxis, + maxTickValue, + minTickValue, + defaultTickCount, ]); return memoizedYAxisOptions; diff --git a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.test.ts b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.test.ts new file mode 100644 index 000000000..58566880d --- /dev/null +++ b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.test.ts @@ -0,0 +1,495 @@ +import type { ColumnMetaData } from '@buster/server-shared/metrics'; +import { DEFAULT_COLUMN_LABEL_FORMAT } from '@buster/server-shared/metrics'; +import { renderHook } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { useYTickValues } from './useYTickValues'; + +describe('useYTickValues', () => { + const mockColumnMetadata: ColumnMetaData[] = [ + { + name: 'revenue', + min_value: 1000, + max_value: 10000, + unique_values: 100, + simple_type: 'number', + type: 'float', + }, + { + name: 'conversion_rate', + min_value: 0.1, + max_value: 0.9, + unique_values: 50, + simple_type: 'number', + type: 'float', + }, + { + name: 'profit_margin', + min_value: -0.05, + max_value: 0.25, + unique_values: 25, + simple_type: 'number', + type: 'float', + }, + ]; + + const defaultProps = { + hasY2Axis: true, + columnMetadata: mockColumnMetadata, + selectedChartType: 'combo' as const, + yAxisKeys: ['revenue'], + y2AxisKeys: ['conversion_rate'], + columnLabelFormats: { + revenue: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const, suffix: '$' }, + conversion_rate: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'percent' as const }, + profit_margin: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'percent' as const }, + }, + }; + + describe('Basic conditions - should return undefined values', () => { + it('should return undefined values when hasY2Axis is false', () => { + const props = { + ...defaultProps, + hasY2Axis: false, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + expect(result.current.minTickValue).toBeUndefined(); + expect(result.current.maxTickValue).toBeUndefined(); + }); + + it('should return undefined values when selectedChartType is not combo', () => { + const props = { + ...defaultProps, + selectedChartType: 'line' as const, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + expect(result.current.minTickValue).toBeUndefined(); + expect(result.current.maxTickValue).toBeUndefined(); + }); + + it('should return undefined values when columnMetadata is undefined', () => { + const props = { + ...defaultProps, + columnMetadata: undefined, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + expect(result.current.minTickValue).toBeUndefined(); + expect(result.current.maxTickValue).toBeUndefined(); + }); + }); + + describe('Percentage value scenarios', () => { + it('should handle all Y values being percentages', () => { + const props = { + ...defaultProps, + yAxisKeys: ['conversion_rate'], + y2AxisKeys: ['profit_margin'], + columnLabelFormats: { + conversion_rate: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'percent' as const }, + profit_margin: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'percent' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // When all values are percentages, it should return the lowest min value + expect(result.current.minTickValue).toBe(-0.05); // profit_margin min_value + // And the highest max value, but at least 1 + expect(result.current.maxTickValue).toBe(1); // Should be at least 1 for percentages + }); + + it('should handle scenario with one percentage value', () => { + const similarRangeMetadata: ColumnMetaData[] = [ + { + name: 'revenue', + min_value: 100, + max_value: 180, // Closer range to percentage values + unique_values: 50, + simple_type: 'number', + type: 'float', + }, + { + name: 'conversion_rate', + min_value: 60, // Similar range to revenue (60-120 vs 100-180) + max_value: 120, + unique_values: 60, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: similarRangeMetadata, + yAxisKeys: ['revenue'], + y2AxisKeys: ['conversion_rate'], + columnLabelFormats: { + revenue: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const, suffix: '$' }, + conversion_rate: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'percent' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // With one percentage value and similar ranges, should apply percentage rounding + // Min ratio: 100/60 = 1.67 < 2 ✓ + // Max ratio: 180/120 = 1.5 < 2 ✓ + expect(result.current.minTickValue).toBeDefined(); + expect(result.current.maxTickValue).toBeDefined(); + }); + + it('should handle percentage values with positive minimum', () => { + const positivePercentageMetadata: ColumnMetaData[] = [ + { + name: 'conversion_rate', + min_value: 0.15, + max_value: 0.85, + unique_values: 50, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: positivePercentageMetadata, + yAxisKeys: ['conversion_rate'], + y2AxisKeys: ['conversion_rate'], + columnLabelFormats: { + conversion_rate: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'percent' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // Should return 0 for positive percentage minimums + expect(result.current.minTickValue).toBe(0); + }); + }); + + describe('Range calculations', () => { + it('should calculate Y-axis and Y2-axis ranges correctly', () => { + const multipleColumnsMetadata: ColumnMetaData[] = [ + { + name: 'revenue', + min_value: 1000, + max_value: 2000, + unique_values: 100, + simple_type: 'number', + type: 'float', + }, + { + name: 'profit', + min_value: 500, + max_value: 1500, + unique_values: 80, + simple_type: 'number', + type: 'float', + }, + { + name: 'conversion_rate', + min_value: 600, // Similar range to Y-axis columns + max_value: 1800, + unique_values: 50, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: multipleColumnsMetadata, + yAxisKeys: ['revenue', 'profit'], // Y-axis range: min=500, max=2000 + y2AxisKeys: ['conversion_rate'], // Y2-axis range: min=600, max=1800 + columnLabelFormats: { + revenue: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + profit: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + conversion_rate: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // Should calculate ranges based on multiple columns + // Min ratio: 600/500 = 1.2 < 2 ✓ + // Max ratio: 2000/1800 = 1.11 < 2 ✓ + expect(result.current.minTickValue).toBeDefined(); + expect(result.current.maxTickValue).toBeDefined(); + }); + + it('should handle missing column metadata gracefully', () => { + const props = { + ...defaultProps, + yAxisKeys: ['nonexistent_column'], + y2AxisKeys: ['another_nonexistent'], + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // When columns don't exist, ranges become infinity initially, then get reset to 0 + // Since both axes have same values (0,0), they're considered similar and return aligned values + expect(result.current.minTickValue).toBe(0); + expect(result.current.maxTickValue).toBe(0); + }); + }); + + describe('Similarity calculations and axis alignment', () => { + it('should align axes when values are similar', () => { + const similarRangeMetadata: ColumnMetaData[] = [ + { + name: 'metric1', + min_value: 100, + max_value: 1000, + unique_values: 50, + simple_type: 'number', + type: 'float', + }, + { + name: 'metric2', + min_value: 110, // Within 2x range + max_value: 1100, // Within 2x range + unique_values: 60, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: similarRangeMetadata, + yAxisKeys: ['metric1'], + y2AxisKeys: ['metric2'], + columnLabelFormats: { + metric1: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + metric2: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // Should align axes when ranges are similar + expect(result.current.minTickValue).toBeDefined(); + expect(result.current.maxTickValue).toBeDefined(); + + // Values should be adjusted with MIN_OFFSET and MAX_OFFSET + expect(result.current.minTickValue).toBe(Math.round(100 * 1.1)); + expect(result.current.maxTickValue).toBe(Math.round(1100 * 1.1)); + }); + + it('should not align axes when values are dissimilar', () => { + const dissimilarRangeMetadata: ColumnMetaData[] = [ + { + name: 'metric1', + min_value: 1, + max_value: 10, + unique_values: 10, + simple_type: 'number', + type: 'float', + }, + { + name: 'metric2', + min_value: 1000, // Much larger, beyond 2x range + max_value: 10000, // Much larger, beyond 2x range + unique_values: 50, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: dissimilarRangeMetadata, + yAxisKeys: ['metric1'], + y2AxisKeys: ['metric2'], + columnLabelFormats: { + metric1: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + metric2: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // Should not align axes when ranges are too different + expect(result.current.minTickValue).toBeUndefined(); + expect(result.current.maxTickValue).toBeUndefined(); + }); + }); + + describe('Edge cases and boundary conditions', () => { + it('should handle zero values correctly', () => { + const zeroValueMetadata: ColumnMetaData[] = [ + { + name: 'metric1', + min_value: 0, + max_value: 0, + unique_values: 1, + simple_type: 'number', + type: 'float', + }, + { + name: 'metric2', + min_value: 0, + max_value: 0, + unique_values: 1, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: zeroValueMetadata, + yAxisKeys: ['metric1'], + y2AxisKeys: ['metric2'], + columnLabelFormats: { + metric1: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + metric2: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // Should handle zero values appropriately + expect(result.current.minTickValue).toBe(0); + expect(result.current.maxTickValue).toBe(0); + }); + + it('should handle negative values correctly', () => { + const negativeValueMetadata: ColumnMetaData[] = [ + { + name: 'metric1', + min_value: -1000, + max_value: -100, + unique_values: 50, + simple_type: 'number', + type: 'float', + }, + { + name: 'metric2', + min_value: -1100, + max_value: -110, + unique_values: 60, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: negativeValueMetadata, + yAxisKeys: ['metric1'], + y2AxisKeys: ['metric2'], + columnLabelFormats: { + metric1: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + metric2: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // Should handle negative values and apply offsets + expect(result.current.minTickValue).toBeDefined(); + expect(result.current.maxTickValue).toBeDefined(); + expect(result.current.minTickValue).toBeLessThan(0); + expect(result.current.maxTickValue).toBeLessThan(0); + }); + + it('should handle empty axis keys', () => { + const props = { + ...defaultProps, + yAxisKeys: [], + y2AxisKeys: [], + }; + + const { result } = renderHook(() => useYTickValues(props)); + + expect(result.current.minTickValue).toBeUndefined(); + expect(result.current.maxTickValue).toBeUndefined(); + }); + }); + + describe('Percentage rounding logic', () => { + it('should round percentage values to nearest 5', () => { + const percentageMetadata: ColumnMetaData[] = [ + { + name: 'conversion1', + min_value: 120, // Scale up so ranges are similar + max_value: 870, + unique_values: 50, + simple_type: 'number', + type: 'float', + }, + { + name: 'revenue1', + min_value: 130, // Similar range to trigger alignment + max_value: 850, + unique_values: 60, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: percentageMetadata, + yAxisKeys: ['conversion1'], + y2AxisKeys: ['revenue1'], + columnLabelFormats: { + conversion1: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'percent' as const }, + revenue1: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // Should round to nearest 5 when there's one percentage value and ranges are similar + // Min: Math.floor(120 * 1.1 / 5) * 5 = Math.floor(26.4) * 5 = 130 + // Max: Math.ceil(870 * 1.1 / 5) * 5 = Math.ceil(191.4) * 5 = 960 + expect(result.current.minTickValue).toBe(130); + expect(result.current.maxTickValue).toBe(960); + }); + + it('should apply regular rounding for non-percentage values', () => { + const nonPercentageMetadata: ColumnMetaData[] = [ + { + name: 'revenue1', + min_value: 1234, + max_value: 5678, + unique_values: 100, + simple_type: 'number', + type: 'float', + }, + { + name: 'revenue2', + min_value: 1345, + max_value: 6789, + unique_values: 120, + simple_type: 'number', + type: 'float', + }, + ]; + + const props = { + ...defaultProps, + columnMetadata: nonPercentageMetadata, + yAxisKeys: ['revenue1'], + y2AxisKeys: ['revenue2'], + columnLabelFormats: { + revenue1: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const, suffix: '$' }, + revenue2: { ...DEFAULT_COLUMN_LABEL_FORMAT, style: 'number' as const, suffix: '$' }, + }, + }; + + const { result } = renderHook(() => useYTickValues(props)); + + // Should apply regular rounding (round to nearest integer) + expect(result.current.minTickValue).toBe(Math.round(1234 * 1.1)); + expect(result.current.maxTickValue).toBe(Math.round(6789 * 1.1)); + }); + }); +}); diff --git a/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.ts b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.ts new file mode 100644 index 000000000..cf395fe44 --- /dev/null +++ b/apps/web/src/components/ui/charts/BusterChartJS/hooks/useOptions/useYTickValues.ts @@ -0,0 +1,206 @@ +/** biome-ignore-all lint/style/noNonNullAssertion: false positive */ + +import round from 'lodash/round'; +import { useMemo } from 'react'; +import type { BusterChartProps } from '../../../BusterChart.types'; + +const MIN_PERCENT_DIFFERENCE = 2; +const MAX_PERCENT_DIFFERENCE = 2; +const MIN_OFFSET = 1.1; +const MAX_OFFSET = 1.1; +const PERCENTAGE_STEP = 5; + +export const useYTickValues = ({ + hasY2Axis, + columnMetadata, + selectedChartType, + yAxisKeys, + y2AxisKeys, + columnLabelFormats, +}: { + hasY2Axis: boolean; + columnMetadata: BusterChartProps['columnMetadata']; + selectedChartType: BusterChartProps['selectedChartType']; + yAxisKeys: string[]; + y2AxisKeys: string[]; + columnLabelFormats: NonNullable; +}) => { + const shouldUseMinAndMaxValues = hasY2Axis && columnMetadata && selectedChartType === 'combo'; + + const checkValues = useMemo(() => { + if (!shouldUseMinAndMaxValues) return []; + return [...yAxisKeys, ...y2AxisKeys]; + }, [yAxisKeys, y2AxisKeys, shouldUseMinAndMaxValues]); + + const hasOnePercentageValue = useMemo(() => { + if (!shouldUseMinAndMaxValues) return false; + return checkValues.some((key) => { + const columnFormat = columnLabelFormats[key]; + return columnFormat?.style === 'percent'; + }); + }, [checkValues, columnLabelFormats, shouldUseMinAndMaxValues]); + + const allYValuesArePercentage = useMemo(() => { + if (!shouldUseMinAndMaxValues) return false; + return checkValues.every((key) => { + const columnFormat = columnLabelFormats[key]; + return columnFormat?.style === 'percent'; + }); + }, [checkValues, columnLabelFormats, shouldUseMinAndMaxValues]); + + const columnMap = useMemo(() => { + if (!columnMetadata || !shouldUseMinAndMaxValues) return new Map(); + return new Map(columnMetadata.map((col) => [col.name, col])); + }, [columnMetadata, shouldUseMinAndMaxValues]); + + // Calculate min/max ranges for y-axis columns + const yAxisRange = useMemo(() => { + if (!shouldUseMinAndMaxValues) return { min: Infinity, max: -Infinity }; + return yAxisKeys.reduce( + (acc, key) => { + const column = columnMap.get(key); + if (!column) return acc; + const min = Number(column.min_value ?? 0); + const max = Number(column.max_value ?? 0); + return { + min: Math.min(acc.min, min), + max: Math.max(acc.max, max), + }; + }, + { min: Infinity, max: -Infinity } + ); + }, [yAxisKeys, columnMap, shouldUseMinAndMaxValues]); + + // Calculate min/max ranges for y2-axis columns + const y2AxisRange = useMemo(() => { + if (!shouldUseMinAndMaxValues) return { min: Infinity, max: -Infinity }; + return y2AxisKeys.reduce( + (acc, key) => { + const column = columnMap.get(key); + if (!column) return acc; + const min = Number(column.min_value ?? 0); + const max = Number(column.max_value ?? 0); + return { + min: Math.min(acc.min, min), + max: Math.max(acc.max, max), + }; + }, + { min: Infinity, max: -Infinity } + ); + }, [y2AxisKeys, columnMap, shouldUseMinAndMaxValues]); + + const minTickValue: number | undefined = useMemo(() => { + if (!shouldUseMinAndMaxValues) return undefined; + + // If all Y values are percentages, return the lowest value + if (allYValuesArePercentage) { + const lowestValue = checkValues.reduce((min, key) => { + const column = columnMap.get(key); + return Math.min(min, Number(column?.min_value ?? 0)); + }, Infinity); + if (lowestValue === Infinity) return undefined; + if (lowestValue > 0) return 0; + return lowestValue; + } + + // Reset infinities if no valid data found + if (yAxisRange.min === Infinity) yAxisRange.min = 0; + if (yAxisRange.max === -Infinity) yAxisRange.max = 0; + if (y2AxisRange.min === Infinity) y2AxisRange.min = 0; + if (y2AxisRange.max === -Infinity) y2AxisRange.max = 0; + + // Check if min values are within 130% of each other + const minValuesAreSimilar = + Math.abs(yAxisRange.min) > 0 && Math.abs(y2AxisRange.min) > 0 + ? Math.max(yAxisRange.min, y2AxisRange.min) / Math.min(yAxisRange.min, y2AxisRange.min) <= + MIN_PERCENT_DIFFERENCE + : yAxisRange.min === y2AxisRange.min; + + // Check if max values are within 130% of each other + const maxValuesAreSimilar = + Math.abs(yAxisRange.max) > 0 && Math.abs(y2AxisRange.max) > 0 + ? Math.max(yAxisRange.max, y2AxisRange.max) / Math.min(yAxisRange.max, y2AxisRange.max) <= + MIN_PERCENT_DIFFERENCE + : yAxisRange.max === y2AxisRange.max; + + // If both min and max values are similar, use the lowest min value + if (minValuesAreSimilar && maxValuesAreSimilar) { + if (hasOnePercentageValue) { + // If there's at least one percentage value, round the min to the nearest lower multiple of 5 + const min = Math.min(yAxisRange.min, y2AxisRange.min) * MIN_OFFSET; + return Math.floor(min / PERCENTAGE_STEP) * PERCENTAGE_STEP; + } + + return round(Math.min(yAxisRange.min, y2AxisRange.min) * MIN_OFFSET, 0); + } + }, [ + hasOnePercentageValue, + columnLabelFormats, + yAxisRange, + y2AxisRange, + shouldUseMinAndMaxValues, + columnMap, + checkValues, + allYValuesArePercentage, + ]); + + const maxTickValue: number | undefined = useMemo(() => { + if (!shouldUseMinAndMaxValues) return undefined; + + // If all Y values are percentages, return the highest value + if (allYValuesArePercentage) { + const highestValue = checkValues.reduce((max, key) => { + const column = columnMap.get(key); + return Math.max(max, Number(column?.max_value ?? 0)); + }, -Infinity); + if (highestValue === -Infinity) return undefined; + if (highestValue < 1) return 1; + return highestValue; + } + + // Reset infinities if no valid data found + if (yAxisRange.min === Infinity) yAxisRange.min = 0; + if (yAxisRange.max === -Infinity) yAxisRange.max = 0; + if (y2AxisRange.min === Infinity) y2AxisRange.min = 0; + if (y2AxisRange.max === -Infinity) y2AxisRange.max = 0; + + // Check if min values are within 150% of each other + const minValuesAreSimilar = + Math.abs(yAxisRange.min) > 0 && Math.abs(y2AxisRange.min) > 0 + ? Math.max(yAxisRange.min, y2AxisRange.min) / Math.min(yAxisRange.min, y2AxisRange.min) <= + MIN_PERCENT_DIFFERENCE + : yAxisRange.min === y2AxisRange.min; + + // Check if max values are within 200% of each other + const maxValuesAreSimilar = + Math.abs(yAxisRange.max) > 0 && Math.abs(y2AxisRange.max) > 0 + ? Math.max(yAxisRange.max, y2AxisRange.max) / Math.min(yAxisRange.max, y2AxisRange.max) <= + MAX_PERCENT_DIFFERENCE + : yAxisRange.max === y2AxisRange.max; + + // If both min and max values are similar, use the highest max value + if (minValuesAreSimilar && maxValuesAreSimilar) { + if (hasOnePercentageValue) { + // If there's at least one percentage value, round the max to the nearest higher multiple of 5 + const max = Math.max(yAxisRange.max, y2AxisRange.max) * MAX_OFFSET; + return Math.ceil(max / PERCENTAGE_STEP) * PERCENTAGE_STEP; + } + + return round(Math.max(yAxisRange.max, y2AxisRange.max) * MAX_OFFSET, 0); + } + }, [ + hasOnePercentageValue, + columnLabelFormats, + yAxisRange, + y2AxisRange, + shouldUseMinAndMaxValues, + columnMap, + checkValues, + allYValuesArePercentage, + ]); + + return { + minTickValue, + maxTickValue, + }; +}; diff --git a/apps/web/src/components/ui/charts/stories/BusterChart.ComboChart.stories.tsx b/apps/web/src/components/ui/charts/stories/BusterChart.ComboChart.stories.tsx index 2d58291f8..6962e747e 100644 --- a/apps/web/src/components/ui/charts/stories/BusterChart.ComboChart.stories.tsx +++ b/apps/web/src/components/ui/charts/stories/BusterChart.ComboChart.stories.tsx @@ -1476,3 +1476,542 @@ export const ComboChartWithNegativeNumbers2: Story = { ], }, }; + +export const ComboChartWithNegativeNumbers3: Story = { + args: { + colors: [ + '#B399FD', + '#FC8497', + '#FBBC30', + '#279EFF', + '#E83562', + '#41F8FF', + '#F3864F', + '#C82184', + '#31FCB4', + '#E83562', + ], + barLayout: 'vertical', + barSortBy: [], + goalLines: [], + gridLines: true, + pieSortBy: 'value', + showLegend: null, + trendlines: [], + scatterAxis: { + x: [], + y: [], + size: [], + tooltip: null, + category: [], + }, + barGroupType: 'group', + metricHeader: null, + pieChartAxis: { + x: [], + y: [], + tooltip: null, + }, + lineGroupType: null, + pieDonutWidth: 40, + xAxisDataZoom: false, + barAndLineAxis: { + x: [], + y: [], + tooltip: null, + category: [], + }, + columnSettings: {}, + comboChartAxis: { + x: ['month', 'year'], + y: ['absolute_change'], + y2: ['percent_change'], + tooltip: null, + category: [], + }, + disableTooltip: false, + metricColumnId: '', + scatterDotSize: [3, 15], + xAxisAxisTitle: null, + yAxisAxisTitle: null, + yAxisScaleType: 'linear', + metricSubHeader: null, + y2AxisAxisTitle: null, + y2AxisScaleType: 'linear', + metricValueLabel: null, + pieLabelPosition: 'none', + tableColumnOrder: null, + barShowTotalAtTop: false, + categoryAxisTitle: null, + pieDisplayLabelAs: 'number', + pieShowInnerLabel: true, + selectedChartType: 'combo', + tableColumnWidths: null, + xAxisTimeInterval: null, + columnLabelFormats: { + year: { + isUTC: false, + style: 'number', + prefix: '', + suffix: '', + currency: 'USD', + columnType: 'number', + dateFormat: 'auto', + multiplier: 1, + displayName: '', + compactNumbers: false, + convertNumberTo: null, + useRelativeTime: false, + numberSeparatorStyle: null, + maximumFractionDigits: 2, + minimumFractionDigits: 0, + makeLabelHumanReadable: true, + replaceMissingDataWith: 0, + }, + month: { + isUTC: false, + style: 'date', + prefix: '', + suffix: '', + currency: 'USD', + columnType: 'number', + dateFormat: 'MMM', + multiplier: 1, + displayName: '', + compactNumbers: false, + convertNumberTo: 'month_of_year', + useRelativeTime: false, + numberSeparatorStyle: null, + maximumFractionDigits: 2, + minimumFractionDigits: 0, + makeLabelHumanReadable: true, + replaceMissingDataWith: 0, + }, + offline_orders: { + isUTC: false, + style: 'number', + prefix: '', + suffix: '', + currency: 'USD', + columnType: 'number', + dateFormat: 'auto', + multiplier: 1, + displayName: 'Offline Orders', + compactNumbers: false, + convertNumberTo: null, + useRelativeTime: false, + numberSeparatorStyle: ',', + maximumFractionDigits: 2, + minimumFractionDigits: 0, + makeLabelHumanReadable: true, + replaceMissingDataWith: 0, + }, + percent_change: { + isUTC: false, + style: 'percent', + prefix: '', + suffix: '', + currency: 'USD', + columnType: 'number', + dateFormat: 'auto', + multiplier: 1, + displayName: 'Percent Change', + compactNumbers: false, + convertNumberTo: null, + useRelativeTime: false, + numberSeparatorStyle: ',', + maximumFractionDigits: 1, + minimumFractionDigits: 1, + makeLabelHumanReadable: true, + replaceMissingDataWith: 0, + }, + absolute_change: { + isUTC: false, + style: 'number', + prefix: '', + suffix: '', + currency: 'USD', + columnType: 'number', + dateFormat: 'auto', + multiplier: 1, + displayName: 'Absolute Change', + compactNumbers: false, + convertNumberTo: null, + useRelativeTime: false, + numberSeparatorStyle: ',', + maximumFractionDigits: 2, + minimumFractionDigits: 0, + makeLabelHumanReadable: true, + replaceMissingDataWith: 0, + }, + prev_month_orders: { + isUTC: false, + style: 'number', + prefix: '', + suffix: '', + currency: 'USD', + columnType: 'number', + dateFormat: 'auto', + multiplier: 1, + displayName: 'Previous Month Orders', + compactNumbers: false, + convertNumberTo: null, + useRelativeTime: false, + numberSeparatorStyle: ',', + maximumFractionDigits: 2, + minimumFractionDigits: 0, + makeLabelHumanReadable: true, + replaceMissingDataWith: 0, + }, + }, + pieInnerLabelTitle: null, + showLegendHeadline: false, + xAxisLabelRotation: 'auto', + xAxisShowAxisLabel: true, + xAxisShowAxisTitle: true, + yAxisShowAxisLabel: true, + yAxisShowAxisTitle: true, + y2AxisShowAxisLabel: true, + y2AxisShowAxisTitle: true, + metricValueAggregate: 'sum', + tableColumnFontColor: null, + tableHeaderFontColor: null, + yAxisStartAxisAtZero: null, + y2AxisStartAxisAtZero: true, + pieInnerLabelAggregate: 'sum', + pieMinimumSlicePercentage: 0, + tableHeaderBackgroundColor: null, + columnMetadata: [ + { + name: 'year', + min_value: 2022, + max_value: 2025, + unique_values: 4, + simple_type: 'number', + type: 'numeric', + }, + { + name: 'month', + min_value: 1, + max_value: 12, + unique_values: 12, + simple_type: 'number', + type: 'numeric', + }, + { + name: 'offline_orders', + min_value: 37, + max_value: 186, + unique_values: 32, + simple_type: 'number', + type: 'int8', + }, + { + name: 'prev_month_orders', + min_value: 37, + max_value: 186, + unique_values: 32, + simple_type: 'number', + type: 'int8', + }, + { + name: 'absolute_change', + min_value: -81, + max_value: 91, + unique_values: 30, + simple_type: 'number', + type: 'int8', + }, + { + name: 'percent_change', + min_value: -46.3, + max_value: 129.7, + unique_values: 34, + simple_type: 'number', + type: 'numeric', + }, + ], + data: [ + { + year: 2022, + month: 9, + offline_orders: 75, + prev_month_orders: 38, + absolute_change: 37, + percent_change: 97.4, + }, + { + year: 2022, + month: 10, + offline_orders: 60, + prev_month_orders: 75, + absolute_change: -15, + percent_change: -20, + }, + { + year: 2022, + month: 11, + offline_orders: 40, + prev_month_orders: 60, + absolute_change: -20, + percent_change: -33.3, + }, + { + year: 2022, + month: 12, + offline_orders: 90, + prev_month_orders: 40, + absolute_change: 50, + percent_change: 125, + }, + { + year: 2023, + month: 1, + offline_orders: 63, + prev_month_orders: 90, + absolute_change: -27, + percent_change: -30, + }, + { + year: 2023, + month: 2, + offline_orders: 40, + prev_month_orders: 63, + absolute_change: -23, + percent_change: -36.5, + }, + { + year: 2023, + month: 3, + offline_orders: 79, + prev_month_orders: 40, + absolute_change: 39, + percent_change: 97.5, + }, + { + year: 2023, + month: 4, + offline_orders: 64, + prev_month_orders: 79, + absolute_change: -15, + percent_change: -19, + }, + { + year: 2023, + month: 5, + offline_orders: 37, + prev_month_orders: 64, + absolute_change: -27, + percent_change: -42.2, + }, + { + year: 2023, + month: 6, + offline_orders: 85, + prev_month_orders: 37, + absolute_change: 48, + percent_change: 129.7, + }, + { + year: 2023, + month: 7, + offline_orders: 68, + prev_month_orders: 85, + absolute_change: -17, + percent_change: -20, + }, + { + year: 2023, + month: 8, + offline_orders: 72, + prev_month_orders: 68, + absolute_change: 4, + percent_change: 5.9, + }, + { + year: 2023, + month: 9, + offline_orders: 139, + prev_month_orders: 72, + absolute_change: 67, + percent_change: 93.1, + }, + { + year: 2023, + month: 10, + offline_orders: 111, + prev_month_orders: 139, + absolute_change: -28, + percent_change: -20.1, + }, + { + year: 2023, + month: 11, + offline_orders: 73, + prev_month_orders: 111, + absolute_change: -38, + percent_change: -34.2, + }, + { + year: 2023, + month: 12, + offline_orders: 133, + prev_month_orders: 73, + absolute_change: 60, + percent_change: 82.2, + }, + { + year: 2024, + month: 1, + offline_orders: 114, + prev_month_orders: 133, + absolute_change: -19, + percent_change: -14.3, + }, + { + year: 2024, + month: 2, + offline_orders: 65, + prev_month_orders: 114, + absolute_change: -49, + percent_change: -43, + }, + { + year: 2024, + month: 3, + offline_orders: 132, + prev_month_orders: 65, + absolute_change: 67, + percent_change: 103.1, + }, + { + year: 2024, + month: 4, + offline_orders: 106, + prev_month_orders: 132, + absolute_change: -26, + percent_change: -19.7, + }, + { + year: 2024, + month: 5, + offline_orders: 74, + prev_month_orders: 106, + absolute_change: -32, + percent_change: -30.2, + }, + { + year: 2024, + month: 6, + offline_orders: 134, + prev_month_orders: 74, + absolute_change: 60, + percent_change: 81.1, + }, + { + year: 2024, + month: 7, + offline_orders: 102, + prev_month_orders: 134, + absolute_change: -32, + percent_change: -23.9, + }, + { + year: 2024, + month: 8, + offline_orders: 95, + prev_month_orders: 102, + absolute_change: -7, + percent_change: -6.9, + }, + { + year: 2024, + month: 9, + offline_orders: 186, + prev_month_orders: 95, + absolute_change: 91, + percent_change: 95.8, + }, + { + year: 2024, + month: 10, + offline_orders: 176, + prev_month_orders: 186, + absolute_change: -10, + percent_change: -5.4, + }, + { + year: 2024, + month: 11, + offline_orders: 99, + prev_month_orders: 176, + absolute_change: -77, + percent_change: -43.8, + }, + { + year: 2024, + month: 12, + offline_orders: 179, + prev_month_orders: 99, + absolute_change: 80, + percent_change: 80.8, + }, + { + year: 2025, + month: 1, + offline_orders: 176, + prev_month_orders: 179, + absolute_change: -3, + percent_change: -1.7, + }, + { + year: 2025, + month: 2, + offline_orders: 96, + prev_month_orders: 176, + absolute_change: -80, + percent_change: -45.5, + }, + { + year: 2025, + month: 3, + offline_orders: 175, + prev_month_orders: 96, + absolute_change: 79, + percent_change: 82.3, + }, + { + year: 2025, + month: 4, + offline_orders: 175, + prev_month_orders: 175, + absolute_change: 0, + percent_change: 0, + }, + { + year: 2025, + month: 5, + offline_orders: 94, + prev_month_orders: 175, + absolute_change: -81, + percent_change: -46.3, + }, + { + year: 2025, + month: 6, + offline_orders: 180, + prev_month_orders: 94, + absolute_change: 86, + percent_change: 91.5, + }, + { + year: 2025, + month: 7, + offline_orders: 181, + prev_month_orders: 180, + absolute_change: 1, + percent_change: 0.6, + }, + ], + }, +}; diff --git a/apps/web/src/components/ui/grid/helpers.ts b/apps/web/src/components/ui/grid/helpers.ts index 6a3da0591..53aca6607 100644 --- a/apps/web/src/components/ui/grid/helpers.ts +++ b/apps/web/src/components/ui/grid/helpers.ts @@ -1,9 +1,19 @@ -export const NUMBER_OF_COLUMNS = 12; -export const MIN_NUMBER_OF_COLUMNS = 3; -export const MAX_NUMBER_OF_COLUMNS = 12; -export const MAX_NUMBER_OF_ITEMS = 4; -export const MIN_ROW_HEIGHT = 320; -export const MAX_ROW_HEIGHT = 550; +import { + MAX_NUMBER_OF_COLUMNS as SERVER_MAX_NUMBER_OF_COLUMNS, + MAX_NUMBER_OF_ITEMS as SERVER_MAX_NUMBER_OF_ITEMS, + MAX_ROW_HEIGHT as SERVER_MAX_ROW_HEIGHT, + MIN_NUMBER_OF_COLUMNS as SERVER_MIN_NUMBER_OF_COLUMNS, + MIN_ROW_HEIGHT as SERVER_MIN_ROW_HEIGHT, + NUMBER_OF_COLUMNS as SERVER_NUMBER_OF_COLUMNS, +} from '@buster/server-shared/dashboards'; + +export const NUMBER_OF_COLUMNS = SERVER_NUMBER_OF_COLUMNS; +export const MIN_NUMBER_OF_COLUMNS = SERVER_MIN_NUMBER_OF_COLUMNS; +export const MAX_NUMBER_OF_COLUMNS = SERVER_MAX_NUMBER_OF_COLUMNS; +export const MAX_NUMBER_OF_ITEMS = SERVER_MAX_NUMBER_OF_ITEMS; +export const MIN_ROW_HEIGHT = SERVER_MIN_ROW_HEIGHT; +export const MAX_ROW_HEIGHT = SERVER_MAX_ROW_HEIGHT; + export const HEIGHT_OF_DROPZONE = 100; export const SASH_SIZE = 12; diff --git a/packages/server-shared/src/dashboards/dashboard-grid.ts b/packages/server-shared/src/dashboards/dashboard-grid.ts new file mode 100644 index 000000000..7d87eafe8 --- /dev/null +++ b/packages/server-shared/src/dashboards/dashboard-grid.ts @@ -0,0 +1,9 @@ +export const MAX_NUMBER_OF_ITEMS_ON_DASHBOARD = 25; +export const NUMBER_OF_COLUMNS = 12; +export const MIN_NUMBER_OF_COLUMNS = 3; +export const MAX_NUMBER_OF_COLUMNS = 12; + +export const MAX_NUMBER_OF_ITEMS = 4; + +export const MIN_ROW_HEIGHT = 320; +export const MAX_ROW_HEIGHT = 550; diff --git a/packages/server-shared/src/dashboards/index.ts b/packages/server-shared/src/dashboards/index.ts index d86bdfbb1..f53de88e5 100644 --- a/packages/server-shared/src/dashboards/index.ts +++ b/packages/server-shared/src/dashboards/index.ts @@ -2,3 +2,4 @@ export * from './dashboard-response.types'; export * from './dashboard.types'; export * from './dashboard-list.types'; export * from './requests.types'; +export * from './dashboard-grid';