Improve chart readability and drilldowns
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m11s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m11s
This commit is contained in:
125
src/chartUtils.ts
Normal file
125
src/chartUtils.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user