mirror of https://github.com/buster-so/buster.git
update data label helrps
This commit is contained in:
parent
3f04b727e5
commit
487dd97e9c
|
@ -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'
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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'
|
||||
});
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
|
@ -1 +1,3 @@
|
|||
export * from './datalabelHelper';
|
||||
export * from './formatChartLabel';
|
||||
export * from './formatBarAndLineDataLabel';
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}),
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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('1 234,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'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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));
|
||||
|
|
Loading…
Reference in New Issue