UNPKG

pagamio-frontend-commons-lib

Version:

Pagamio library for Frontend reusable components like the form engine and table container

473 lines (472 loc) 19.5 kB
/** * A generic utility to build x-axis values and series data for a stacked bar chart in ECharts. * * @param data - An array of objects of any shape (T). * @param xAxisDataValueKey - The string key in each object representing x-axis data (e.g. "channel", "status"). * @param seriesDataNameKey - The string key in each object representing the series name (e.g. "productName"). * @param seriesDataValueKey - The string key in each object representing the numeric value (e.g. "amount"). * * @returns An object containing: * - xAxisValues: A string[] of unique x-axis values (in order). * - seriesData: An array of SeriesItem (with "name", "type", "stack", and the numeric data array). */ export function getBarChartData(data, xAxisDataValueKey, seriesDataNameKey, seriesDataValueKey) { // 1. Extract unique x-axis values (in order of appearance). const xAxis = []; for (const item of data) { const xVal = String(item[xAxisDataValueKey] ?? ''); if (!xAxis.includes(xVal)) { xAxis.push(xVal); } } // 2. Extract unique series names (in order of appearance). const seriesNames = []; for (const item of data) { const seriesName = String(item[seriesDataNameKey] ?? ''); if (!seriesNames.includes(seriesName)) { seriesNames.push(seriesName); } } // 3. Create a mapping: seriesName -> number[] (index matches xAxis order). const productAmountMap = {}; seriesNames.forEach((name) => { productAmountMap[name] = Array(xAxis.length).fill(0); }); // 4. Populate the mapping using the data. for (const item of data) { const xVal = String(item[xAxisDataValueKey] ?? ''); const seriesName = String(item[seriesDataNameKey] ?? ''); const amount = Number(item[seriesDataValueKey] ?? 0); const xIndex = xAxis.indexOf(xVal); if (xIndex !== -1 && productAmountMap[seriesName]) { productAmountMap[seriesName][xIndex] = amount; } } // 5. Construct the series data array for ECharts. const seriesData = seriesNames.map((name) => ({ name, type: 'bar', stack: 'total', data: productAmountMap[name], })); return { xAxisValues: xAxis, seriesData, }; } /** * Flattens your data (if it happens to be an array of arrays) * and maps it into an xAxis array of names and a seriesData * array of { name, value } objects. * * @param data The raw data from the backend, e.g. [ [ { amount: 60, productName: 'Electronics' }, ... ] ] * If data is just a single array (e.g. [ { amount: 60, productName: 'Electronics' }, ... ]), * this function will still work (no harm in calling `flat()`). * @param nameKey The key in each object that maps to the category name (used in xAxis and the `name` field). * @param valueKey The key in each object that maps to the numeric value (used in `value`). * * @returns An object containing: * - xAxis: string[] (e.g. ["Electronics", "Fashion", "Home & Garden"]) * - seriesData: Array<{ name: string; value: number }> * (e.g. [{ name: "Electronics", value: 60 }, ...]) */ export function getPieChartData(data, // can be an array of objects OR an array of arrays of objects nameKey, // e.g. 'productName' valueKey, // e.g. 'amount' otherKey) { // Flatten in case the response is nested, e.g. [ [ { ...}, { ...} ] ] const flattened = Array.isArray(data[0]) ? data.flat() : data; const getOtherKeyValue = (item) => { let result = 0; if (otherKey) { let convertedValue; if (typeof item[otherKey] === 'number') { convertedValue = Number(item[otherKey]); } else { convertedValue = String(item[otherKey]); } result = convertedValue; } return result; }; // Build xAxis and seriesData const xAxis = flattened.map((item) => String(item[nameKey])); const seriesData = flattened.map((item) => ({ name: String(item[nameKey]), value: Number(item[valueKey]) || 0, otherValue: getOtherKeyValue(item), })); return { xAxis, seriesData }; } /** * Flattens your data (if it happens to be an array of arrays) * and maps it into an xAxis array of names and a seriesData * array of { name, value } objects. * * @param data The raw data from the backend, e.g. [ [ { amount: 60, productName: 'Electronics' }, ... ] ] * If data is just a single array (e.g. [ { amount: 60, productName: 'Electronics' }, ... ]), * this function will still work (no harm in calling `flat()`). * @param nameKey The key in each object that maps to the category name (used in xAxis and the `name` field). * @param valueKey The key in each object that maps to the numeric value (used in `value`). * * @returns An object containing: * - seriesData: Array<{ name: string; value: number }> * (e.g. [{ name: "Electronics", value: 60 }, ...]) */ export function getSeriesData(data, // can be an array of objects OR an array of arrays of objects nameKey, // e.g. 'productName' valueKey) { // Flatten in case the response is nested, e.g. [ [ { ...}, { ...} ] ] const flattened = Array.isArray(data[0]) ? data.flat() : data; // Build seriesData const seriesData = flattened.map((item) => ({ name: String(item[nameKey]), value: Number(item[valueKey]) || 0, })); return { seriesData }; } /** * Flattens your data (if it happens to be an array of arrays) * and maps it into a select options array. Handles null/undefined values safely. * * @param data The raw data from the backend. * @param labelKey The key in each object that maps to the label in the options. * @param valueKey Optional key for the value in the options. If not provided, labelKey will be used for both label and value. * @returns An object containing optionsData array with { label, value } pairs. */ export function getFilterOptionsData(data, labelKey, valueKey) { // Handle empty or null data if (!data || !data.length) { return { optionsData: [] }; } // Flatten in case the response is nested, e.g. [ [ { ...}, { ...} ] ] const flattened = Array.isArray(data[0]) ? data.flat() : data; // Use a Set to track unique labels and values const uniqueLabels = new Set(); const uniqueValues = new Set(); // Build optionsData, ensuring uniqueness and handling null values const optionsData = flattened.reduce((acc, item) => { // Skip null/undefined items if (!item) return acc; const rawLabelValue = item[labelKey]; // Skip if the labelKey value is null/undefined if (rawLabelValue == null) return acc; const label = String(rawLabelValue); const value = valueKey && item[valueKey] != null ? String(item[valueKey]) : label; // Check if the label or value is already in the Set if (!uniqueLabels.has(label) && !uniqueValues.has(value)) { uniqueLabels.add(label); uniqueValues.add(value); acc.push({ label, value }); } return acc; }, []); return { optionsData }; } /** * Flattens your data (if it happens to be an array of arrays) * and maps it into a select options array. */ export function generateSelectOptions(data, labelKey, valueKey) { // Flatten in case the response is nested, e.g. [ [ { ...}, { ...} ] ] const flattened = Array.isArray(data[0]) ? data.flat() : data; // Use a Set to track unique labels and values const uniqueLabels = new Set(); const uniqueValues = new Set(); // Build optionsData, ensuring uniqueness const optionsData = flattened.reduce((acc, item) => { const label = String(item[labelKey]); const value = String(item[valueKey]); // Check if the label or value is already in the Set if (!uniqueLabels.has(label) && !uniqueValues.has(value)) { uniqueLabels.add(label); uniqueValues.add(value); acc.push({ label, value }); } return acc; }, []); return { optionsData }; } export const formatValue = (value, format, options) => { if (format === 'currency') { const currency = options?.currency || 'ZAR'; const locale = options?.locale || 'en-ZA'; return new Intl.NumberFormat(locale, { style: 'currency', currency, }).format(value); } // Default formatting for numbers return value.toString(); }; /** * Standardized date formatting function for the entire application. * Formats dates to a consistent locale-specific format. * * @param date - Date string, Date object, or null/undefined * @param options - Optional formatting options * @returns Formatted date string or fallback value */ export const formatDate = (date, options) => { const { locale = 'en-US', fallback = 'N/A', includeTime = false } = options || {}; if (!date) return fallback; try { const dateObj = typeof date === 'string' ? new Date(date) : date; if (Number.isNaN(dateObj.getTime())) { console.warn('Invalid date provided to formatDate:', date); return fallback; } if (includeTime) { return dateObj.toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', }); } return dateObj.toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', }); } catch (error) { console.error('Error formatting date:', error); return fallback; } }; /** * Formats a date range as a string. * * @param startDate - Start date * @param endDate - End date * @param options - Optional formatting options * @returns Formatted date range string */ export const formatDateRange = (startDate, endDate, options) => { const { separator = ' - ', fallback = 'N/A' } = options || {}; const formattedStart = formatDate(startDate, options); const formattedEnd = formatDate(endDate, options); if (formattedStart === (options?.fallback || 'N/A') || formattedEnd === (options?.fallback || 'N/A')) { return fallback; } return `${formattedStart}${separator}${formattedEnd}`; }; /** * Truncates long IDs to show first N and last N characters with a separator. * * @param id - ID string, number, or null/undefined * @param options - Optional truncation options * @returns Truncated ID string or fallback value */ export const truncateId = (id, options) => { const { startChars = 5, endChars = 5, separator = '…', minLength = 10, fallback = '' } = options || {}; // Handle null, undefined, or empty values if (id === null || id === undefined || id === '') { return fallback; } // Convert to string if it's a number const value = String(id); // Return as-is if shorter than minimum length if (value.length <= minLength) { return value; } // Truncate if longer than minimum length return `${value.slice(0, startChars)}${separator}${value.slice(-endChars)}`; }; export const transformColumns = (columns) => { return columns.map((col) => ({ accessorKey: col.accessor, header: col.header, enableCopy: col.enableCopy, formattedCellElement: col.formattedCellElement ? col.formattedCellElement : undefined, })); }; /** * Formats an array of objects into DataPoint format required for Distribution Chart * @param data Array of objects to format * @param valueKey Key in the object to use as the value * @param nameKey Key in the object to use as the name * @returns Array of DataPoint objects */ export function formatToDataPoints(data, valueKey, nameKey) { return data.map((item) => ({ value: Number(item[valueKey]) || 0, // Convert to number, default to 0 if invalid name: String(item[nameKey] || ''), // Convert to string, default to empty string if undefined })); } export const formatDonutChartData = (data, nameKey, valueKey) => { if (!data?.length) return { legendData: [], seriesData: [] }; // Extract the legend data (product names) const legendData = data.map((item) => item[nameKey]); // Format series data for the donut chart const seriesData = data.map((item) => { const dataPoint = { name: item[nameKey], value: item[valueKey], }; return dataPoint; }); return { legendData, seriesData }; }; /** * Transforms flat data into heatmap format with proper axis categories and data matrix. * * @param data - Array of objects containing the data * @param xAxisKey - Key for x-axis categories (e.g., 'region', 'dayOfWeek') * @param yAxisKey - Key for y-axis categories (e.g., 'product', 'hour') * @param valueKey - Key for the numeric values to be displayed as heat intensity * @returns Object with xAxis categories, yAxis categories, and heatmap data matrix */ export function getHeatmapData(data, xAxisKey, yAxisKey, valueKey) { if (!data?.length) { return { xAxis: [], yAxis: [], data: [] }; } // Extract unique categories for both axes const xAxisCategories = Array.from(new Set(data.map((item) => String(item[xAxisKey] || '')))); const yAxisCategories = Array.from(new Set(data.map((item) => String(item[yAxisKey] || '')))); // Create heatmap data matrix: [xIndex, yIndex, value] const heatmapData = data.map((item) => { const xIndex = xAxisCategories.indexOf(String(item[xAxisKey] || '')); const yIndex = yAxisCategories.indexOf(String(item[yAxisKey] || '')); const value = Number(item[valueKey]) || 0; return [xIndex, yIndex, value]; }); return { xAxis: xAxisCategories, yAxis: yAxisCategories, data: heatmapData, }; } /** * Calculates the week number of the year for a given date. * @param date - The date to calculate the week number for * @returns The week number as a string (e.g., "Week 1") */ function calculateWeekNumber(date) { const startOfYear = new Date(date.getFullYear(), 0, 1); const weekNumber = Math.ceil(((date.getTime() - startOfYear.getTime()) / (1000 * 60 * 60 * 24) + startOfYear.getDay() + 1) / 7); return `Week ${weekNumber}`; } /** * Determines the value to use based on the valueKey type. * @param item - The data item * @param valueKey - The key to extract value from * @returns The computed value (1 for counting, or actual value) */ function determineValue(item, valueKey) { return valueKey === 'transactionId' || valueKey === 'id' ? 1 : Number(item[valueKey]) || 1; } /** * Processes a single data item to extract date information. * @param item - The data item to process * @param dateKey - The key for the date field * @param valueKey - The key for the value field * @returns Processed date information or null if invalid date */ function processDataItem(item, dateKey, valueKey) { const dateStr = String(item[dateKey] || ''); const date = new Date(dateStr); if (Number.isNaN(date.getTime())) { return null; } const dayOfWeek = date.toLocaleDateString('en-US', { weekday: 'long' }); const weekOfYear = calculateWeekNumber(date); const key = date.toISOString().split('T')[0]; // YYYY-MM-DD format const value = determineValue(item, valueKey); return { key, dayOfWeek, weekOfYear, value }; } /** * Generates the final heatmap data matrix from processed date data. * @param dataByDate - Map of processed date data * @param daysOfWeek - Array of day names * @param allWeeks - Array of week names * @returns Heatmap data matrix */ function generateHeatmapMatrix(dataByDate, daysOfWeek, allWeeks) { const heatmapData = []; for (let weekIndex = 0; weekIndex < allWeeks.length; weekIndex++) { const week = allWeeks[weekIndex]; for (let dayIndex = 0; dayIndex < daysOfWeek.length; dayIndex++) { const day = daysOfWeek[dayIndex]; const matchingEntry = Array.from(dataByDate.values()).find((entry) => entry.dayOfWeek === day && entry.weekOfYear === week); heatmapData.push([dayIndex, weekIndex, matchingEntry ? matchingEntry.value : 0]); } } return heatmapData; } /** * Transforms date-based data into a day-of-week vs week heatmap format. * Generic utility that can be used for any date field and value combination. * * This function supports two calling patterns: * 1. Direct usage: createDayWeekHeatmapData(data, dateKey, valueKey) * 2. HeatmapChart adapter: createDayWeekHeatmapData(data, xAxisKey, yAxisKey, valueKey) * - In this case, yAxisKey is used as the date field, xAxisKey is ignored * * @param data - Array of objects containing date and value data * @param dateKeyOrXAxisKey - Key for the date field, or xAxisKey (ignored in 4-param version) * @param valueKeyOrYAxisKey - Key for values, or yAxisKey (used as date field in 4-param version) * @param optionalValueKey - Optional valueKey (used in 4-param version) * @returns Object with day-of-week x-axis, week y-axis, and heatmap data matrix */ export function createDayWeekHeatmapData(data, dateKeyOrXAxisKey, valueKeyOrYAxisKey, optionalValueKey) { // Handle both function signatures let dateKey; let valueKey; if (optionalValueKey) { // 4-parameter version: (data, xAxisKey, yAxisKey, valueKey) // For date-based heatmaps, yAxisKey is the date field, xAxisKey is ignored dateKey = valueKeyOrYAxisKey; valueKey = optionalValueKey; } else { // 3-parameter version: (data, dateKey, valueKey) dateKey = dateKeyOrXAxisKey; valueKey = valueKeyOrYAxisKey; } if (!data?.length) { return { xAxis: [], yAxis: [], data: [] }; } const dataByDate = new Map(); // Process the data to group by date and extract day/week info for (const item of data) { const processedItem = processDataItem(item, dateKey, valueKey); if (!processedItem) continue; const { key, dayOfWeek, weekOfYear, value } = processedItem; const existingEntry = dataByDate.get(key); if (existingEntry) { existingEntry.value += 1; } else { dataByDate.set(key, { dayOfWeek, weekOfYear, value, originalDate: key, }); } } // Generate complete day and week ranges const daysOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; const allWeeks = Array.from(new Set(Array.from(dataByDate.values()).map((d) => d.weekOfYear))).sort((a, b) => { const weekA = Number.parseInt(a.replace('Week ', ''), 10); const weekB = Number.parseInt(b.replace('Week ', ''), 10); return weekA - weekB; }); // Create complete heatmap data with zeros for missing combinations const heatmapData = generateHeatmapMatrix(dataByDate, daysOfWeek, allWeeks); return { xAxis: daysOfWeek, yAxis: allWeeks, data: heatmapData, }; }