UNPKG

chart-data-grouper

Version:

A utility to group, sum, average and transform data into Chart.js format with nested and date support.

359 lines (358 loc) 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = groupByDate; const getValueByPath_1 = require("./getValueByPath"); const formatDate_1 = require("./formatDate"); function getWeekStartDate(date) { // Clone the date to avoid modifying original const d = new Date(date); // Get day of week (0 = Sunday, 6 = Saturday) const day = d.getDay(); // Calculate difference to previous Sunday const diff = d.getDate() - day; // Set to start of week (Sunday) d.setDate(diff); // Set to start of day d.setHours(0, 0, 0, 0); return d; } function generateDateIntervals(start, end, timeGrouping) { const intervals = []; const current = new Date(start); while (current <= end) { const date = new Date(current); let groupKey; if (timeGrouping && (0, formatDate_1.isValidFormat)(timeGrouping)) { groupKey = (0, formatDate_1.default)(date, timeGrouping); } else { const year = date.getFullYear(); const month = date.getMonth(); const day = date.getDate(); switch (timeGrouping) { case 'milliseconds': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}:${String(date.getMilliseconds()).padStart(3, '0')}`; break; case 'seconds': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`; break; case 'minutes': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; break; case 'hours': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(date.getHours()).padStart(2, '0')}`; break; case 'days': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; break; case 'weeks': const weekStart = getWeekStartDate(date); if (timeGrouping && (0, formatDate_1.isValidFormat)(timeGrouping)) { groupKey = (0, formatDate_1.default)(weekStart, timeGrouping); } else { groupKey = `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')}`; } break; case 'months': groupKey = `${year}-${String(month + 1).padStart(2, '0')}`; break; case 'years': groupKey = `${year}`; break; default: groupKey = date.toISOString().split('T')[0]; } } intervals.push({ key: groupKey, date }); if (timeGrouping.includes('SSS') || timeGrouping === 'milliseconds') { current.setMilliseconds(current.getMilliseconds() + 1); } else if (timeGrouping.includes('ss') || timeGrouping === 'seconds') { current.setSeconds(current.getSeconds() + 1); } else if (timeGrouping.includes('mm') || timeGrouping === 'minutes') { current.setMinutes(current.getMinutes() + 1); } else if (timeGrouping.includes('HH') || timeGrouping === 'hours') { current.setHours(current.getHours() + 1); } else if (timeGrouping.includes('DD') || timeGrouping === 'days') { current.setDate(current.getDate() + 1); } else if (timeGrouping.includes('W') || timeGrouping === 'weeks') { current.setDate(current.getDate() + 7); } else if (timeGrouping.includes('MM') || timeGrouping === 'months') { current.setMonth(current.getMonth() + 1); } else if (timeGrouping.includes('YYYY') || timeGrouping === 'years') { current.setFullYear(current.getFullYear() + 1); } else { current.setDate(current.getDate() + 1); // Default to daily increment for custom formats } } return intervals; } function groupByDate(rawData, options) { const { dateField, valueFields, operation = 'sum', timeGrouping = 'months', emptyIntervalFill, startDate: startDateOption, endDate: endDateOption, } = options; const grouped = {}; // grouped['count'] = [0]; // Determine date range let minDate = null; let maxDate = null; let data = rawData; const startDateProvided = startDateOption !== undefined; const endDateProvided = endDateOption !== undefined; if (startDateProvided || endDateProvided) { data = rawData.filter(item => { const dateValue = (0, getValueByPath_1.default)(item, dateField); const date = typeof dateValue === 'string' || dateValue instanceof Date ? new Date(dateValue) : null; return date && (!startDateProvided || date >= new Date(startDateOption)) && (!endDateProvided || date <= new Date(endDateOption)); }); } // Find min and max dates from data if not provided data.forEach(item => { const dateValue = (0, getValueByPath_1.default)(item, dateField); const date = typeof dateValue === 'string' || dateValue instanceof Date ? new Date(dateValue) : null; if (!date || isNaN(date.getTime())) return; if (!minDate || date < minDate) minDate = new Date(date); if (!maxDate || date > maxDate) maxDate = new Date(date); }); // Use provided dates if available const startDate = startDateOption ? new Date(startDateOption) : minDate; const endDate = endDateOption ? new Date(endDateOption) : maxDate; if (!startDate || !endDate) { return []; } // Generate all intervals if emptyIntervalFill is specified const allIntervals = emptyIntervalFill !== undefined ? generateDateIntervals(startDate, endDate, timeGrouping) : []; // Initialize empty intervals if needed if (emptyIntervalFill !== undefined) { allIntervals.forEach(({ key }) => { if (!grouped[key]) { grouped[key] = { date: key }; if (operation === 'count') { grouped[key]['count'] = 0; } else { valueFields.forEach(field => { grouped[key][field] = []; }); } } }); } // Process data data.forEach(item => { const dateValue = (0, getValueByPath_1.default)(item, dateField); const date = typeof dateValue === 'string' || dateValue instanceof Date ? new Date(dateValue) : null; if (!date || isNaN(date.getTime())) return; let groupKey; if (timeGrouping && (0, formatDate_1.isValidFormat)(timeGrouping)) { groupKey = (0, formatDate_1.default)(date, timeGrouping); } else { const year = date.getFullYear(); const month = date.getMonth(); const day = date.getDate(); switch (timeGrouping) { case 'milliseconds': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}:${String(date.getMilliseconds()).padStart(3, '0')}`; break; case 'seconds': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`; break; case 'minutes': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; break; case 'hours': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}T${String(date.getHours()).padStart(2, '0')}`; break; case 'days': groupKey = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; break; case 'weeks': const weekStart = getWeekStartDate(date); if (timeGrouping && (0, formatDate_1.isValidFormat)(timeGrouping)) { groupKey = (0, formatDate_1.default)(weekStart, timeGrouping); } else { groupKey = `${weekStart.getFullYear()}-${String(weekStart.getMonth() + 1).padStart(2, '0')}-${String(weekStart.getDate()).padStart(2, '0')}`; } break; case 'months': groupKey = `${year}-${String(month + 1).padStart(2, '0')}`; break; case 'years': groupKey = `${year}`; break; default: groupKey = date.toISOString().split('T')[0]; } } if (!grouped[groupKey]) { grouped[groupKey] = { date: groupKey }; if (operation === 'count') { grouped[groupKey]['count'] = 0; } else { valueFields.forEach(field => { grouped[groupKey][field] = []; }); } } if (operation === 'count') { grouped[groupKey]['count'] += 1; } else { valueFields.forEach(field => { const value = (0, getValueByPath_1.default)(item, field); if (value !== undefined && value !== null) { grouped[groupKey][field].push(Number(value)); } }); } }); // Calculate aggregated values let result = Object.values(grouped).map(group => { const output = { date: group.date }; if (operation === 'count') { output['count'] = group['count']; return output; } valueFields.forEach(field => { const values = group[field].filter((v) => !isNaN(v)); if (values.length === 0) { output[field] = null; return; } switch (operation) { case 'sum': output[field] = values.reduce((a, b) => a + b, 0); break; case 'average': output[field] = +(values.reduce((a, b) => a + b, 0) / values.length).toFixed(2); break; case 'max': output[field] = Math.max(...values); break; case 'min': output[field] = Math.min(...values); break; default: output[field] = values.reduce((a, b) => a + b, 0); } }); return output; }); // Sort by date const intervalsWithDates = emptyIntervalFill !== undefined ? allIntervals : Object.keys(grouped).map(key => ({ key, date: new Date(key.includes('T') ? key : key.includes('-') ? key.length === 4 ? `${key}-01-01` // Year only : key.length === 7 ? `${key}-01` // Year-month : key // Year-month-day : key) })); function parseFormattedDate(dateStr, format) { let year = 0, month = 0, day = 1, hours = 0, minutes = 0, seconds = 0, ms = 0; // Helper to extract value by token const extract = (token) => { const pos = format.indexOf(token); return pos >= 0 ? dateStr.slice(pos, pos + token.length) : ''; }; // Extract components const yyyy = extract('YYYY'); const mmmm = extract('MMMM'); const mmm = extract('MMM'); const mm = extract('MM'); const dd = extract('DD'); const hh = extract('HH'); const mi = extract('mm'); const ss = extract('ss'); const sss = extract('SSS'); // Set year if exists if (yyyy) year = parseInt(yyyy, 10); // Set month (prioritize longer tokens) if (mmmm) { month = new Date(`${mmmm} 1, 2000`).getMonth(); } else if (mmm) { month = new Date(`${mmm} 1, 2000`).getMonth(); } else if (mm) { month = parseInt(mm, 10) - 1; } // Set other components if (dd) day = parseInt(dd, 10); if (hh) hours = parseInt(hh, 10); if (mi) minutes = parseInt(mi, 10); if (ss) seconds = parseInt(ss, 10); if (sss) ms = parseInt(sss, 10); return new Date(year, month, day, hours, minutes, seconds, ms); } result = result.sort((a, b) => { if ((0, formatDate_1.isValidFormat)(timeGrouping)) { const dateA = parseFormattedDate(a.date, timeGrouping); const dateB = parseFormattedDate(b.date, timeGrouping); return dateA.getTime() - dateB.getTime(); } const dateA = intervalsWithDates.find(i => i.key === a.date)?.date; const dateB = intervalsWithDates.find(i => i.key === b.date)?.date; return (dateA?.getTime() || 0) - (dateB?.getTime() || 0); }); // Handle empty interval filling if (emptyIntervalFill !== undefined) { let previousValues = {}; if (operation === 'count') { previousValues['count'] = 0; } else { valueFields.forEach(field => { previousValues[field] = 0; }); } result = result.map(item => { const output = { date: item.date }; if (operation === 'count') { output['count'] = item['count']; previousValues['count'] = item['count']; } else { valueFields.forEach(field => { if (item[field] !== null) { output[field] = item[field]; previousValues[field] = item[field]; } else { output[field] = emptyIntervalFill === 0 ? 0 : previousValues[field] !== undefined ? previousValues[field] : 0; } }); } return output; }); } return result; }