Files
food-tracer/client/src/pages/statsShared.ts
T
Ondřej Anděl efed5e6e86 fix: graf cena per zdroj nahrazen koláčem
feat: stránka doplnit údaje předělána na samostatnou stránku jídel + přidáno přejmenování
2026-10-12 09:49:41 +02:00

92 lines
3.8 KiB
TypeScript

import { StatsRange } from "../enums";
import { formatDateString, getHumanMonth, shiftDate } from "../Utils";
/** Vzhled bubliny s hodnotou — recharts ji kreslí inline stylem, ne přes CSS třídu. */
export const TOOLTIP_STYLE = {
background: 'var(--ft-bg-card)',
border: '1px solid var(--ft-border)',
borderRadius: 'var(--ft-radius-sm)',
color: 'var(--ft-text)',
};
/** Hranice zvoleného období statistik, YYYY-MM-DD včetně. */
export type StatsBounds = { from: string, to: string };
/**
* Popisek období na ose grafu — den u měsíce, měsíc u roku, rok u celého přehledu.
*
* @param period označení období ze serveru (YYYY-MM-DD, YYYY-MM nebo YYYY)
* @param mode zvolený rozsah statistik
*/
export function formatPeriodLabel(period: string, mode: StatsRange): string {
if (mode === StatsRange.MESIC) return formatDateString(period).slice(0, 5);
if (mode === StatsRange.ROK) return getHumanMonth(period).split(' ')[0];
return period;
}
/** Vybere řadu součtů podle rozsahu — dny u měsíce, měsíce u roku, roky u celého přehledu. */
export function pickPeriods<T>(stats: { byDay: T[], byMonth: T[], byYear: T[] }, mode: StatsRange): T[] {
if (mode === StatsRange.MESIC) return stats.byDay;
if (mode === StatsRange.ROK) return stats.byMonth;
return stats.byYear;
}
/**
* Vrátí označení všech období rozsahu, i těch bez dat — prázdný den v grafu
* je informace sama o sobě.
*
* U "Vše" se začíná rokem prvního dne s daty, jinak by graf otevíralo deset let nul.
*
* @param range zobrazené období
* @param mode rozsah statistik
* @param firstDate první den s daty, YYYY-MM-DD; bez něj je u "Vše" výsledek prázdný
*/
export const listPeriods = (range: StatsBounds, mode: StatsRange, firstDate?: string): string[] => {
if (mode === StatsRange.MESIC) {
const periods: string[] = [];
for (let date = range.from; date <= range.to; date = shiftDate(date, 1)) periods.push(date);
return periods;
}
if (mode === StatsRange.ROK) {
const year = range.from.substring(0, 4);
return Array.from({ length: 12 }, (_, index) => `${year}-${String(index + 1).padStart(2, '0')}`);
}
if (!firstDate) return [];
const first = Number(firstDate.substring(0, 4));
const last = Number(range.to.substring(0, 4));
return Array.from({ length: last - first + 1 }, (_, index) => String(first + index));
};
/** Zkrátí datum dne na označení období grafu. */
export const periodOf = (date: string, mode: StatsRange): string => {
if (mode === StatsRange.MESIC) return date;
if (mode === StatsRange.ROK) return date.substring(0, 7);
return date.substring(0, 4);
};
/** Kolik zdrojů má v koláči vlastní výseč; zbytek se slije do "Ostatní". */
const SOURCE_SLICE_COUNT = 7;
/** Výseč koláče zdrojů. */
export type SourceSlice = { name: string, value: number, color: string };
/**
* Sestaví výseče koláče dle zdroje — příjmu i útraty. Barva jde podle pořadí ve
* výběru, zdrojů bývá hodně a pevná barva pro každý by paletu nestačila.
*
* @param bySource součty dle zdroje, sestupně dle zobrazované hodnoty
* @param getValue zobrazovaná hodnota součtu (kcal, Kč)
*/
export const buildSourceSlices = <T extends { key: string }>(
bySource: T[],
getValue: (total: T) => number,
): SourceSlice[] => {
const slices = bySource
.filter(total => getValue(total) > 0)
.slice(0, SOURCE_SLICE_COUNT)
.map((total, index) => ({ name: total.key, value: getValue(total), color: `var(--ft-series-${index + 1})` }));
const rest = bySource.slice(SOURCE_SLICE_COUNT).reduce((sum, total) => sum + getValue(total), 0);
if (rest > 0) slices.push({ name: 'Ostatní', value: rest, color: 'var(--ft-series-other)' });
return slices;
};