update data label helrps

This commit is contained in:
Nate Kelley 2025-04-01 17:00:05 -06:00
parent 3f04b727e5
commit 487dd97e9c
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
11 changed files with 364 additions and 60 deletions

View File

@ -1,62 +1,7 @@
import { formatLabel } from '@/lib/columnFormatter';
import type { Context } from 'chartjs-plugin-datalabels';
import { formatChartLabelDelimiter } from '../../commonHelpers';
import { extractFieldsFromChain, appendToKeyValueChain } from '../../chartHooks';
import {
type BusterChartProps,
type ColumnLabelFormat
} from '@/api/asset_interfaces/metric/charts';
import { determineFontColorContrast } from '@/lib/colors';
export const dataLabelFontColorContrast = (context: Context) => {
const color = context.dataset.backgroundColor as string;
return determineFontColorContrast(color);
};
export const formatChartLabel = (
label: string,
columnLabelFormats: NonNullable<BusterChartProps['columnLabelFormats']>,
hasMultipleMeasures: boolean,
hasCategoryAxis: boolean
): string => {
if (hasCategoryAxis && !hasMultipleMeasures) {
const fields = extractFieldsFromChain(label);
const lastField = fields.at(0)!;
const newLabel = appendToKeyValueChain(lastField);
return formatChartLabelDelimiter(newLabel, columnLabelFormats);
}
if (!hasMultipleMeasures) {
const fields = extractFieldsFromChain(label);
const lastField = fields.at(-1)!;
return formatChartLabelDelimiter(lastField.value || lastField.key, columnLabelFormats);
}
return formatChartLabelDelimiter(label, columnLabelFormats);
};
export const formatBarAndLineDataLabel = (
value: number,
context: Context,
usePercentage: boolean | undefined,
columnLabelFormat: ColumnLabelFormat
) => {
if (!usePercentage) {
return formatLabel(value, columnLabelFormat);
}
const shownDatasets = context.chart.data.datasets.filter(
(dataset) => !dataset.hidden && !dataset.isTrendline
);
const hasMultipleDatasets = shownDatasets.length > 1;
const total: number = hasMultipleDatasets
? context.chart.$totalizer.stackTotals[context.dataIndex]
: context.chart.$totalizer.seriesTotals[context.datasetIndex];
const percentage = ((value as number) / total) * 100;
return formatLabel(percentage, {
...columnLabelFormat,
style: 'percent',
columnType: 'number'
});
};

View File

@ -0,0 +1,36 @@
import { formatBarAndLineDataLabel } from './formatBarAndLineDataLabel';
import { ColumnLabelFormat, IColumnLabelFormat } from '@/api/asset_interfaces/metric';
import { Context } from 'chartjs-plugin-datalabels';
describe('formatBarAndLineDataLabel', () => {
it('formats a single value without percentage', () => {
const value = 1234.56;
const columnLabelFormat: ColumnLabelFormat = {
style: 'number',
columnType: 'number',
numberSeparatorStyle: ',',
minimumFractionDigits: 0,
maximumFractionDigits: 2
};
const mockContext = {
chart: {
data: {
datasets: []
},
$totalizer: {
stackTotals: [],
seriesTotals: []
}
},
active: false,
dataIndex: 0,
dataset: {},
datasetIndex: 0
} as unknown as Context;
const result = formatBarAndLineDataLabel(value, mockContext, false, columnLabelFormat);
expect(result).toBe('1,234.56');
});
});

View File

@ -0,0 +1,29 @@
import { ColumnLabelFormat } from '@/api/asset_interfaces/metric';
import { formatLabel } from '@/lib/columnFormatter';
import type { Context } from 'chartjs-plugin-datalabels';
export const formatBarAndLineDataLabel = (
value: number,
context: Context,
usePercentage: boolean | undefined,
columnLabelFormat: ColumnLabelFormat
) => {
if (!usePercentage) {
return formatLabel(value, columnLabelFormat);
}
const shownDatasets = context.chart.data.datasets.filter(
(dataset) => !dataset.hidden && !dataset.isTrendline
);
const hasMultipleDatasets = shownDatasets.length > 1;
const total: number = hasMultipleDatasets
? context.chart.$totalizer.stackTotals[context.dataIndex]
: context.chart.$totalizer.seriesTotals[context.datasetIndex];
const percentage = ((value as number) / total) * 100;
return formatLabel(percentage, {
...columnLabelFormat,
style: 'percent',
columnType: 'number'
});
};

View File

@ -0,0 +1,76 @@
import { formatChartLabel } from './formatChartLabel';
import type { IColumnLabelFormat } from '@/api/asset_interfaces/metric/charts';
describe('formatChartLabel', () => {
it('should format label correctly when hasCategoryAxis is true and hasMultipleMeasures is false', () => {
const columnLabelFormats = {
month: {
style: 'date',
compactNumbers: false,
columnType: 'date',
displayName: '',
numberSeparatorStyle: ',',
minimumFractionDigits: 0,
maximumFractionDigits: 2,
currency: 'USD',
convertNumberTo: null,
dateFormat: 'MMM YYYY',
useRelativeTime: false,
isUTC: false,
multiplier: 1,
prefix: '',
suffix: '',
replaceMissingDataWith: null,
makeLabelHumanReadable: true
},
recent_total: {
style: 'currency',
compactNumbers: false,
columnType: 'number',
displayName: '',
numberSeparatorStyle: ',',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: 'USD',
convertNumberTo: null,
dateFormat: 'auto',
useRelativeTime: false,
isUTC: false,
multiplier: 1,
prefix: '',
suffix: '',
replaceMissingDataWith: 0,
makeLabelHumanReadable: true
},
long_term_avg: {
style: 'currency',
compactNumbers: false,
columnType: 'number',
displayName: '',
numberSeparatorStyle: ',',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: 'USD',
convertNumberTo: null,
dateFormat: 'auto',
useRelativeTime: false,
isUTC: false,
multiplier: 1,
prefix: '',
suffix: '',
replaceMissingDataWith: 0,
makeLabelHumanReadable: true
}
} satisfies Record<string, IColumnLabelFormat>;
// Act
let result = formatChartLabel('recent_total__🔑__', columnLabelFormats, true, false);
expect(result).toBe('Recent Total');
result = formatChartLabel('recent_total__🔑__', columnLabelFormats, false, false);
expect(result).toBe('Recent Total');
result = formatChartLabel('recent_total__🔑__', columnLabelFormats, false, true);
expect(result).toBe('Recent Total');
});
});

View File

@ -0,0 +1,25 @@
import type { BusterChartProps } from '@/api/asset_interfaces';
import { extractFieldsFromChain, appendToKeyValueChain } from '../../chartHooks';
import { formatChartLabelDelimiter } from '../../commonHelpers';
export const formatChartLabel = (
label: string,
columnLabelFormats: NonNullable<BusterChartProps['columnLabelFormats']>,
hasMultipleMeasures: boolean,
hasCategoryAxis: boolean
): string => {
if (hasCategoryAxis && !hasMultipleMeasures) {
const fields = extractFieldsFromChain(label);
const lastField = fields.at(0)!;
const newLabel = appendToKeyValueChain(lastField);
return formatChartLabelDelimiter(newLabel, columnLabelFormats);
}
if (!hasMultipleMeasures) {
const fields = extractFieldsFromChain(label);
const lastField = fields.at(-1)!;
return formatChartLabelDelimiter(lastField.value || lastField.key, columnLabelFormats);
}
return formatChartLabelDelimiter(label, columnLabelFormats);
};

View File

@ -1 +1,3 @@
export * from './datalabelHelper';
export * from './formatChartLabel';
export * from './formatBarAndLineDataLabel';

View File

@ -0,0 +1,103 @@
import { formatChartValueDelimiter } from './labelHelpers';
import type { ColumnLabelFormat } from '@/api/asset_interfaces/metric/charts';
describe('formatChartValueDelimiter', () => {
it('should format a numeric value using the column format', () => {
const rawValue = 1234.56;
const columnNameDelimiter = 'value__🔑__';
const columnLabelFormats: Record<string, ColumnLabelFormat> = {
value: {
style: 'number',
columnType: 'number',
minimumFractionDigits: 1,
maximumFractionDigits: 1
}
};
const result = formatChartValueDelimiter(rawValue, columnNameDelimiter, columnLabelFormats);
expect(result).toBe('1,234.6');
});
it('If the key is not found, it will fallback to the default format', () => {
const rawValue = 1234.56;
const columnNameDelimiter = 'THIS_IS_INVALID_KEY';
const columnLabelFormats: Record<string, ColumnLabelFormat> = {
value: {
style: 'number',
columnType: 'number',
minimumFractionDigits: 1,
maximumFractionDigits: 1
}
};
const result = formatChartValueDelimiter(rawValue, columnNameDelimiter, columnLabelFormats);
expect(result).toBe('1234.56');
});
it('should return a valid dollar format', () => {
const rawValue = 3363690.3966666665;
const columnNameDelimiter = 'long_term_avg__🔑__';
const columnLabelFormats: Record<string, ColumnLabelFormat> = {
month: {
style: 'date',
compactNumbers: false,
columnType: 'date',
displayName: '',
numberSeparatorStyle: ',',
minimumFractionDigits: 0,
maximumFractionDigits: 2,
currency: 'USD',
convertNumberTo: null,
dateFormat: 'MMM YYYY',
useRelativeTime: false,
isUTC: false,
multiplier: 1,
prefix: '',
suffix: '',
replaceMissingDataWith: null,
makeLabelHumanReadable: true
},
recent_total: {
style: 'currency',
compactNumbers: false,
columnType: 'number',
displayName: '',
numberSeparatorStyle: ',',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: 'USD',
convertNumberTo: null,
dateFormat: 'auto',
useRelativeTime: false,
isUTC: false,
multiplier: 1,
prefix: '',
suffix: '',
replaceMissingDataWith: 0,
makeLabelHumanReadable: true
},
long_term_avg: {
style: 'currency',
compactNumbers: false,
columnType: 'number',
displayName: '',
numberSeparatorStyle: ',',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: 'USD',
convertNumberTo: null,
dateFormat: 'auto',
useRelativeTime: false,
isUTC: false,
multiplier: 1,
prefix: '',
suffix: '',
replaceMissingDataWith: 0,
makeLabelHumanReadable: true
}
};
const result = formatChartValueDelimiter(rawValue, columnNameDelimiter, columnLabelFormats);
expect(result).toBe('$3,363,690.40');
});
});

View File

@ -22,7 +22,7 @@ const chatRouteRecord: Record<AllFileTypes, (chatId: string, assetId: string) =>
}),
metric: (chatId, assetId) =>
createBusterRoute({
route: BusterRoutes.APP_CHAT_ID_METRIC_ID,
route: BusterRoutes.APP_CHAT_ID_METRIC_ID_CHART,
chatId,
metricId: assetId
}),

View File

@ -1,3 +1,4 @@
import { ColumnLabelFormat } from '@/api/asset_interfaces/metric';
import { formatLabel } from './columnFormatter';
describe('formatLabel', () => {
@ -133,6 +134,32 @@ describe('formatLabel', () => {
})
).toBe('');
});
it('should handle complex dollar formatting', () => {
const rawValue = 3363690.3966666665;
const config = {
style: 'currency',
compactNumbers: false,
columnType: 'number',
displayName: '',
numberSeparatorStyle: ',',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
currency: 'USD',
convertNumberTo: null,
dateFormat: 'auto',
useRelativeTime: false,
isUTC: false,
multiplier: 1,
prefix: '',
suffix: '',
replaceMissingDataWith: 0,
makeLabelHumanReadable: true
} satisfies ColumnLabelFormat;
const result = formatLabel(rawValue, config);
expect(result).toBe('$3,363,690.40');
});
});
describe('string formatting', () => {

View File

@ -0,0 +1,60 @@
import { formatNumber } from './numbers';
describe('formatNumber', () => {
test('handles null and undefined values', () => {
expect(formatNumber(null)).toBe('');
expect(formatNumber(undefined)).toBe('');
});
test('formats basic numbers', () => {
expect(formatNumber(1234.5678)).toBe('1,234.568');
expect(formatNumber(1000)).toBe('1,000');
expect(formatNumber(0)).toBe('0');
});
test('respects decimal places settings', () => {
expect(formatNumber(1234.5678, { minDecimals: 3 })).toBe('1,234.568');
expect(formatNumber(1234.5, { minDecimals: 3 })).toBe('1,234.500');
expect(formatNumber(1234.5678, { maximumDecimals: 1 })).toBe('1,234.6');
});
test('handles compact notation', () => {
expect(formatNumber(1_000_000, { compact: true })).toBe('1M');
expect(formatNumber(1_500_000, { compact: true })).toBe('1.5M');
expect(formatNumber(1_000, { compact: true })).toBe('1K');
});
test('handles different locales', () => {
expect(formatNumber(1234.56, { locale: 'de-DE' })).toBe('1.234,56');
expect(formatNumber(1234.56, { locale: 'fr-FR' })).toBe('1234,56');
});
test('handles currency formatting', () => {
expect(formatNumber(1234.56, { currency: 'USD' })).toBe('$1,234.56');
expect(formatNumber(1234.56, { currency: 'EUR' })).toBe('€1,234.56');
});
test('handles string inputs', () => {
expect(formatNumber('1234.56')).toBe('1,234.56');
expect(formatNumber('1000')).toBe('1,000');
});
test('handles non-numeric strings', () => {
expect(formatNumber('not a number')).toBe('not a number');
expect(formatNumber('')).toBe('');
});
test('handles grouping options', () => {
expect(formatNumber(1234567, { useGrouping: false })).toBe('1234567');
expect(formatNumber(1234567, { useGrouping: true })).toBe('1,234,567');
});
test('handles compact dollar', () => {
const roundedNumber = 3363690.4;
const currency = 'USD';
const compactNumbers = false;
expect(formatNumber(roundedNumber, { currency, compact: compactNumbers })).toBe(
'$3,363,690.40'
);
});
});

View File

@ -60,12 +60,13 @@ export const formatNumber = (
);
const formatter = new Intl.NumberFormat(locale, {
...options,
minimumFractionDigits: options?.minDecimals || options?.minimumFractionDigits || 0,
minimumFractionDigits: options?.minDecimals,
maximumFractionDigits: maxFractionDigits,
notation: options?.compact ? 'compact' : undefined,
notation: options?.compact ? 'compact' : 'standard',
compactDisplay: 'short',
style: options?.currency ? 'currency' : undefined
style: options?.currency ? 'currency' : 'decimal',
currency: options?.currency,
useGrouping: options?.useGrouping !== false
});
return formatter.format(Number(value));