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
JavaScript
/**
* 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,
};
}