mirror of https://github.com/buster-so/buster.git
Merge pull request #1133 from buster-so/big-nate-bus-1933-negative-data-being-drawn-outside-of-chart
Big nate bus 1933 negative data being drawn outside of chart
This commit is contained in:
commit
6309dff5f7
|
@ -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;
|
|
@ -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[],
|
||||
|
|
|
@ -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[],
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -141,6 +141,7 @@ export const useOptions = ({
|
|||
y2AxisScaleType,
|
||||
y2AxisStartAxisAtZero,
|
||||
columnMetadata,
|
||||
yAxis,
|
||||
});
|
||||
|
||||
const isHorizontalBar = useMemo(() => {
|
||||
|
|
|
@ -22,6 +22,10 @@ describe('useY2Axis', () => {
|
|||
y2AxisScaleType: 'linear' as const,
|
||||
columnMetadata: [],
|
||||
columnSettings: {},
|
||||
yAxis: {
|
||||
min: undefined,
|
||||
max: undefined,
|
||||
},
|
||||
} as Parameters<typeof useY2Axis>[0];
|
||||
|
||||
it('should return undefined display when chart type is not Combo', () => {
|
||||
|
|
|
@ -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<ChartConfigProps['columnLabelFormats']>;
|
||||
selectedAxis: ChartEncodes;
|
||||
|
@ -36,9 +43,12 @@ export const useY2Axis = ({
|
|||
y2AxisStartAxisAtZero: BusterChartProps['y2AxisStartAxisAtZero'];
|
||||
y2AxisScaleType: BusterChartProps['y2AxisScaleType'];
|
||||
columnMetadata: NonNullable<BusterChartProps['columnMetadata']>;
|
||||
yAxis: DeepPartial<ScaleOptionsByType<keyof CartesianScaleTypeRegistry> | undefined>;
|
||||
}): DeepPartial<ScaleChartOptions<'bar'>['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,
|
||||
|
|
|
@ -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<ChartConfigProps['columnLabelFormats']>;
|
||||
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<GridLineOptions> | undefined = useMemo(() => {
|
||||
return {
|
||||
display: gridLines,
|
||||
|
@ -157,11 +128,9 @@ export const useYAxis = ({
|
|||
const memoizedYAxisOptions: DeepPartial<ScaleChartOptions<'bar'>['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<ScaleChartOptions<'bar'>['scales']['y']>;
|
||||
|
||||
return baseConfig;
|
||||
}, [
|
||||
tickCallback,
|
||||
type,
|
||||
|
@ -191,6 +159,9 @@ export const useYAxis = ({
|
|||
yAxisStartAxisAtZero,
|
||||
yAxisShowAxisLabel,
|
||||
usePercentageModeAxis,
|
||||
maxTickValue,
|
||||
minTickValue,
|
||||
defaultTickCount,
|
||||
]);
|
||||
|
||||
return memoizedYAxisOptions;
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<BusterChartProps['columnLabelFormats']>;
|
||||
}) => {
|
||||
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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue