Files
graphs/src/chartUtils.ts
Cauê Faleiros 948931886e
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m11s
Improve chart readability and drilldowns
2026-07-14 13:55:59 -03:00

126 lines
4.5 KiB
TypeScript

import type { DateRange } from './types';
export type DateBucket = 'day' | 'week' | 'month';
const MS_PER_DAY = 24 * 60 * 60 * 1000;
export const getRangeDayCount = (dateRange: DateRange) => (
Math.max(1, Math.ceil((dateRange.end.getTime() - dateRange.start.getTime()) / MS_PER_DAY) + 1)
);
export const getAutoDateBucket = (dateRange: DateRange): DateBucket => {
const days = getRangeDayCount(dateRange);
if (days > 365) return 'month';
if (days > 90) return 'week';
return 'day';
};
export const parseChartDate = (value: string): Date | null => {
if (!value) return null;
const isoMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoMatch) {
const [, year, month, day] = isoMatch;
return new Date(Number(year), Number(month) - 1, Number(day));
}
const localMatch = value.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (localMatch) {
const [, day, month, year] = localMatch;
return new Date(Number(year), Number(month) - 1, Number(day));
}
const dashedLocalMatch = value.match(/^(\d{2})-(\d{2})-(\d{4})$/);
if (dashedLocalMatch) {
const [, day, month, year] = dashedLocalMatch;
return new Date(Number(year), Number(month) - 1, Number(day));
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
};
export const formatChartDateKey = (date: Date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const startOfWeek = (date: Date) => {
const nextDate = new Date(date);
const day = nextDate.getDay();
const offset = day === 0 ? -6 : 1 - day;
nextDate.setDate(nextDate.getDate() + offset);
nextDate.setHours(0, 0, 0, 0);
return nextDate;
};
export const getDateBucketKey = (value: string, bucket: DateBucket) => {
const date = parseChartDate(value);
if (!date) return value;
if (bucket === 'day') return formatChartDateKey(date);
if (bucket === 'week') return formatChartDateKey(startOfWeek(date));
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
};
export const formatDateBucketLabel = (value: string, bucket: DateBucket) => {
if (bucket === 'month') {
const match = value.match(/^(\d{4})-(\d{2})$/);
if (!match) return value;
const [, year, month] = match;
return new Intl.DateTimeFormat('pt-BR', { month: 'short', year: '2-digit' }).format(new Date(Number(year), Number(month) - 1, 1));
}
const date = parseChartDate(value);
if (!date) return value;
if (bucket === 'week') {
const endDate = new Date(date);
endDate.setDate(endDate.getDate() + 6);
const formatter = new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit' });
return `${formatter.format(date)}-${formatter.format(endDate)}`;
}
return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit' }).format(date);
};
export const formatDateBucketLongLabel = (value: string, bucket: DateBucket) => {
if (bucket === 'month') {
const match = value.match(/^(\d{4})-(\d{2})$/);
if (!match) return value;
const [, year, month] = match;
return new Intl.DateTimeFormat('pt-BR', { month: 'long', year: 'numeric' }).format(new Date(Number(year), Number(month) - 1, 1));
}
const date = parseChartDate(value);
if (!date) return value;
if (bucket === 'week') {
const endDate = new Date(date);
endDate.setDate(endDate.getDate() + 6);
const formatter = new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' });
return `${formatter.format(date)} - ${formatter.format(endDate)}`;
}
return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date);
};
export const getMovingAverageWindow = (bucket: DateBucket) => {
if (bucket === 'day') return 7;
if (bucket === 'week') return 4;
return 3;
};
export const averageRecentValues = (values: number[], endIndex: number, windowSize: number) => {
const startIndex = Math.max(0, endIndex - windowSize + 1);
const windowValues = values.slice(startIndex, endIndex + 1);
if (!windowValues.length) return 0;
return windowValues.reduce((total, value) => total + value, 0) / windowValues.length;
};
export const normalizeUnknownLabel = (value: string, fallback: string) => {
const normalized = String(value || '').replace(/\s+/g, ' ').trim();
if (!normalized) return fallback;
if (/^(0|null|undefined|unknown|n\/a|-|sem nome)$/i.test(normalized)) return fallback;
return normalized;
};