datastake-daf 0.6.845 → 0.6.847

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,251 +2,268 @@ import dayjs from 'dayjs';
2
2
  import { capitalize } from '../../helpers/StringHelper.js';
3
3
  import { renderNumber } from './numbers.js';
4
4
 
5
- /**
6
- * Formats a date based on the time filter
7
- * @param {dayjs.Dayjs} date - The date to format
8
- * @param {boolean} breakLine - Whether to add a line break (for tooltips)
9
- * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly', 'yearly')
10
- * @returns {string} Formatted date string
11
- */
12
5
  export const getFormatDate = (date, breakLine = false, timeFilter = 'monthly') => {
13
- switch (timeFilter) {
14
- case "daily":
15
- return date.format("DD/MM");
16
- case "weekly":
17
- return `W${renderNumber(date.week())}`;
18
- case "yearly":
19
- return date.format("YYYY");
20
- default:
21
- // Monthly format: "Dec 24", "Jan 25", etc.
22
- return breakLine
23
- ? `${capitalize(date.format("MMM"))}\n${date.format("YY")}`
24
- : `${capitalize(date.format("MMM"))} ${date.format("YY")}`;
25
- }
6
+ switch (timeFilter) {
7
+ case "daily":
8
+ return date.format("DD/MM");
9
+ case "weekly":
10
+ return `W${renderNumber(date.week())}`;
11
+ case "yearly":
12
+ return date.format("YYYY");
13
+ default:
14
+ // Monthly format: "Dec 24", "Jan 25", etc.
15
+ return breakLine
16
+ ? `${capitalize(date.format("MMM"))}\n${date.format("YY")}`
17
+ : `${capitalize(date.format("MMM"))} ${date.format("YY")}`;
18
+ }
26
19
  };
27
20
 
28
- /**
29
- * Gets the time quantity string for dayjs operations
30
- * @param {string} timeFilter - The time filter ('daily', 'weekly', 'monthly', 'yearly')
31
- * @returns {string} Time quantity string ('days', 'weeks', 'months', 'years')
32
- */
33
21
  export const getTimeQuantity = (timeFilter = 'monthly') => {
34
- switch (timeFilter) {
35
- case "daily":
36
- return "days";
37
- case "weekly":
38
- return "weeks";
39
- case "yearly":
40
- return "years";
41
- default:
42
- return "months";
43
- }
22
+ switch (timeFilter) {
23
+ case "daily":
24
+ return "days";
25
+ case "weekly":
26
+ return "weeks";
27
+ case "yearly":
28
+ return "years";
29
+ default:
30
+ return "months";
31
+ }
44
32
  };
45
33
 
46
- /**
47
- * Gets previous cumulative score from data before start date
48
- * @param {Array} dates - Array of data objects with date field
49
- * @param {dayjs.Dayjs} startDate - The start date
50
- * @param {string} timeFilter - The time filter
51
- * @param {string} valueField - The field name to extract value from (default: 'total')
52
- * @returns {Object} Object with hasPreviousData, previousCumulativeScore, previousMaxScore
53
- */
54
34
  export const getPreviousGraphData = (dates, startDate, timeFilter, valueField = 'total') => {
55
- let previousCumulativeScore = 0;
56
- let previousMaxScore = 0;
57
- let hasPreviousData = false;
58
-
59
- dates.forEach((d) => {
60
- const date = dayjs(d.date, "YYYY-MM-DD");
61
- if (!date.isValid()) return;
62
-
63
- let isBeforeStart = false;
64
- switch (timeFilter) {
65
- case "daily":
66
- isBeforeStart = date.isBefore(startDate, 'day');
67
- break;
68
- case "weekly":
69
- isBeforeStart = date.isBefore(startDate, 'week');
70
- break;
71
- case "yearly":
72
- isBeforeStart = date.isBefore(startDate, 'year');
73
- break;
74
- default:
75
- isBeforeStart = date.isBefore(startDate, 'month');
76
- break;
77
- }
78
-
79
- if (isBeforeStart) {
80
- hasPreviousData = true;
81
- const value = Number(d[valueField] || d.count || d.jobs || d.value || 0) || 0;
82
- previousCumulativeScore += value;
83
- previousMaxScore = Math.max(previousMaxScore, value);
84
- }
85
- });
86
-
87
- return {
88
- hasPreviousData,
89
- previousCumulativeScore,
90
- previousMaxScore,
91
- };
35
+ let previousCumulativeScore = 0;
36
+ let previousMaxScore = 0;
37
+ let hasPreviousData = false;
38
+
39
+ dates.forEach((d) => {
40
+ const date = dayjs(d.date, "YYYY-MM-DD");
41
+ if (!date.isValid()) return;
42
+
43
+ let isBeforeStart = false;
44
+ switch (timeFilter) {
45
+ case "daily":
46
+ isBeforeStart = date.isBefore(startDate, 'day');
47
+ break;
48
+ case "weekly":
49
+ isBeforeStart = date.isBefore(startDate, 'week');
50
+ break;
51
+ case "yearly":
52
+ isBeforeStart = date.isBefore(startDate, 'year');
53
+ break;
54
+ default:
55
+ isBeforeStart = date.isBefore(startDate, 'month');
56
+ break;
57
+ }
58
+
59
+ if (isBeforeStart) {
60
+ hasPreviousData = true;
61
+ const value = Number(d[valueField] || d.count || d.jobs || d.value || 0) || 0;
62
+ previousCumulativeScore += value;
63
+ previousMaxScore = Math.max(previousMaxScore, value);
64
+ }
65
+ });
66
+
67
+ return {
68
+ hasPreviousData,
69
+ previousCumulativeScore,
70
+ previousMaxScore,
71
+ };
92
72
  };
93
73
 
94
- /**
95
- * Processes chart data with time filtering support
96
- * @param {Object} params - Parameters object
97
- * @param {Array} params.mainData - Array of data objects with date and value fields
98
- * @param {string} params.timeFilter - Time filter ('daily', 'weekly', 'monthly', 'yearly')
99
- * @param {Object} params.filters - Optional filters object with timeframe
100
- * @param {boolean} params.isCumulative - Whether to calculate cumulative values (default: false)
101
- * @param {string} params.valueField - Field name to extract value from (default: 'total', also checks 'count', 'jobs', 'value')
102
- * @returns {Array} Processed chart data array
103
- */
104
- export const processChartDateData = ({
105
- mainData,
106
- timeFilter: filter,
107
- filters = {},
108
- isCumulative = false,
109
- valueField = 'total'
74
+ export const processChartDateData = ({
75
+ mainData,
76
+ timeFilter: filter,
77
+ filters = {},
78
+ isCumulative = false,
79
+ valueField = 'total'
110
80
  }) => {
111
- const timeQuantity = getTimeQuantity(filter);
112
- const dates = mainData || [];
113
- const isEmpty = !mainData || !Array.isArray(mainData) || mainData.length === 0;
114
-
115
- const _data = [];
116
- let end = filters?.timeframe?.endDate || dayjs();
117
- let start = filters?.timeframe?.startDate || dayjs().add(-12, timeQuantity);
118
-
119
- // Normalize start and end to period boundaries
120
- if (filter === "daily") {
121
- start = start.startOf('day');
122
- end = end.startOf('day');
123
- } else if (filter === "weekly") {
124
- start = start.startOf('week');
125
- end = end.startOf('week');
126
- } else if (filter === "yearly") {
127
- start = start.startOf('year');
128
- end = end.startOf('year');
129
- } else {
130
- start = start.startOf('month');
131
- end = end.startOf('month');
132
- }
133
-
134
- let i = 0;
135
- let cumulativeScore = 0;
136
-
137
- // Calculate initial cumulative value if needed
138
- if (isCumulative) {
139
- const { hasPreviousData, previousCumulativeScore } = getPreviousGraphData(
140
- dates,
141
- start,
142
- filter,
143
- valueField
144
- );
145
- cumulativeScore = hasPreviousData ? previousCumulativeScore : 0;
146
- }
147
-
148
- // Get the period unit for comparison
149
- const getPeriodUnit = (f) => {
150
- switch (f) {
151
- case "daily": return "day";
152
- case "weekly": return "week";
153
- case "yearly": return "year";
154
- default: return "month";
155
- }
156
- };
157
- const periodUnit = getPeriodUnit(filter);
158
-
159
- // Loop until we reach the end date
160
- let currentDate = start.clone();
161
- while (currentDate.isBefore(end) || currentDate.isSame(end, periodUnit)) {
162
- // Filter data points that fall within this period
163
- const score = isEmpty ? 0 : dates
164
- .filter((d) => {
165
- if (!d.date) return false;
166
- switch (filter) {
167
- case "daily":
168
- return d.date === currentDate.format("YYYY-MM-DD");
169
- case "weekly":
170
- return dayjs(d.date, "YYYY-MM-DD").week() === currentDate.week() &&
171
- dayjs(d.date, "YYYY-MM-DD").year() === currentDate.year();
172
- case "yearly":
173
- return dayjs(d.date, "YYYY-MM-DD").year() === currentDate.year();
174
- default:
175
- return (
176
- dayjs(d.date, "YYYY-MM-DD").format("YYYY-MM") ===
177
- currentDate.format("YYYY-MM")
178
- );
179
- }
180
- })
181
- .reduce((a, b) => {
182
- const value = Number(b[valueField] || b.count || b.jobs || b.value || 0) || 0;
183
- return a + value;
184
- }, 0);
185
-
186
- if (isCumulative) {
187
- cumulativeScore += score;
188
- }
189
-
190
- _data.push({
191
- date: getFormatDate(currentDate, false, filter),
192
- value: isCumulative ? cumulativeScore : score,
193
- period: score, // Period value for tooltip
194
- jobs: score, // For compatibility with jobs field
195
- month: currentDate.format('YYYY-MM-DD'), // For compatibility
196
- key: i,
197
- });
198
-
199
- currentDate = currentDate.add(1, timeQuantity);
200
- i++;
201
- }
202
-
203
- return _data;
81
+ const timeQuantity = getTimeQuantity(filter);
82
+ const dates = mainData || [];
83
+ const isEmpty = !mainData || !Array.isArray(mainData) || mainData.length === 0;
84
+
85
+ const _data = [];
86
+
87
+ // Derive date range from actual data if no filters provided and data exists
88
+ let end, start;
89
+
90
+ if (filters?.timeframe?.endDate) {
91
+ end = filters.timeframe.endDate;
92
+ } else if (!isEmpty) {
93
+ // Find the latest date in the data
94
+ const sortedDates = dates
95
+ .map(d => dayjs(d.date, "YYYY-MM-DD"))
96
+ .filter(d => d.isValid())
97
+ .sort((a, b) => b.valueOf() - a.valueOf());
98
+ end = sortedDates.length > 0 ? sortedDates[0] : dayjs();
99
+ } else {
100
+ end = dayjs();
101
+ }
102
+
103
+ if (filters?.timeframe?.startDate) {
104
+ start = filters.timeframe.startDate;
105
+ } else if (!isEmpty) {
106
+ // Find the earliest date in the data
107
+ const sortedDates = dates
108
+ .map(d => dayjs(d.date, "YYYY-MM-DD"))
109
+ .filter(d => d.isValid())
110
+ .sort((a, b) => a.valueOf() - b.valueOf());
111
+ start = sortedDates.length > 0 ? sortedDates[0] : dayjs().add(-12, timeQuantity);
112
+
113
+ // For daily filter, limit to last 14 days to avoid overcrowded x-axis
114
+ if (filter === "daily" && end) {
115
+ const maxDailyStart = end.subtract(13, 'days'); // 14 days total including end date
116
+ if (start.isBefore(maxDailyStart)) {
117
+ start = maxDailyStart;
118
+ }
119
+ }
120
+ } else {
121
+ start = dayjs().add(-12, timeQuantity);
122
+ }
123
+
124
+ // Normalize start and end to period boundaries
125
+ if (filter === "daily") {
126
+ start = start.startOf('day');
127
+ end = end.startOf('day');
128
+ } else if (filter === "weekly") {
129
+ start = start.startOf('week');
130
+ end = end.startOf('week');
131
+ } else if (filter === "yearly") {
132
+ start = start.startOf('year');
133
+ end = end.startOf('year');
134
+ } else {
135
+ start = start.startOf('month');
136
+ end = end.startOf('month');
137
+ }
138
+
139
+ // Ensure minimum number of periods to fill the x-axis properly
140
+ const getMinPeriods = (f) => {
141
+ switch (f) {
142
+ case "daily": return 14; // At least 14 days
143
+ case "weekly": return 8; // At least 8 weeks
144
+ case "yearly": return 5; // At least 5 years
145
+ default: return 12; // At least 12 months
146
+ }
147
+ };
148
+ const minPeriods = getMinPeriods(filter);
149
+ const periodsDiff = end.diff(start, timeQuantity);
150
+
151
+ if (periodsDiff < minPeriods - 1) {
152
+ const periodsToAdd = minPeriods - 1 - periodsDiff;
153
+ start = start.subtract(periodsToAdd, timeQuantity);
154
+ }
155
+
156
+ let i = 0;
157
+ let cumulativeScore = 0;
158
+
159
+ // Calculate initial cumulative value if needed
160
+ if (isCumulative) {
161
+ const { hasPreviousData, previousCumulativeScore } = getPreviousGraphData(
162
+ dates,
163
+ start,
164
+ filter,
165
+ valueField
166
+ );
167
+ cumulativeScore = hasPreviousData ? previousCumulativeScore : 0;
168
+ }
169
+
170
+ // Get the period unit for comparison
171
+ const getPeriodUnit = (f) => {
172
+ switch (f) {
173
+ case "daily": return "day";
174
+ case "weekly": return "week";
175
+ case "yearly": return "year";
176
+ default: return "month";
177
+ }
178
+ };
179
+ const periodUnit = getPeriodUnit(filter);
180
+
181
+ // Loop until we reach the end date
182
+ let currentDate = start.clone();
183
+ while (currentDate.isBefore(end) || currentDate.isSame(end, periodUnit)) {
184
+ // Filter data points that fall within this period
185
+ const score = isEmpty ? 0 : dates
186
+ .filter((d) => {
187
+ if (!d.date) return false;
188
+ switch (filter) {
189
+ case "daily":
190
+ return d.date === currentDate.format("YYYY-MM-DD");
191
+ case "weekly":
192
+ return dayjs(d.date, "YYYY-MM-DD").week() === currentDate.week() &&
193
+ dayjs(d.date, "YYYY-MM-DD").year() === currentDate.year();
194
+ case "yearly":
195
+ return dayjs(d.date, "YYYY-MM-DD").year() === currentDate.year();
196
+ default:
197
+ return (
198
+ dayjs(d.date, "YYYY-MM-DD").format("YYYY-MM") ===
199
+ currentDate.format("YYYY-MM")
200
+ );
201
+ }
202
+ })
203
+ .reduce((a, b) => {
204
+ const value = Number(b[valueField] || b.count || b.jobs || b.value || 0) || 0;
205
+ return a + value;
206
+ }, 0);
207
+
208
+ if (isCumulative) {
209
+ cumulativeScore += score;
210
+ }
211
+
212
+ _data.push({
213
+ date: getFormatDate(currentDate, false, filter),
214
+ value: isCumulative ? cumulativeScore : score,
215
+ period: score, // Period value for tooltip
216
+ jobs: score, // For compatibility with jobs field
217
+ month: currentDate.format('YYYY-MM-DD'), // For compatibility
218
+ key: i,
219
+ });
220
+
221
+ currentDate = currentDate.add(1, timeQuantity);
222
+ i++;
223
+ }
224
+
225
+ return _data;
204
226
  };
205
227
 
206
- /**
207
- * Formats date axis labels, checking if already formatted
208
- * @param {string} label - The label to format
209
- * @param {Function} getFormatDateFn - Function to format dates
210
- * @returns {string} Formatted date string
211
- */
212
228
  export const formatDateAxis = (label, getFormatDateFn) => {
213
- if (!label) return label;
214
-
215
- // Check if label is already in the correct format (MMM YY, DD/MM, W#, or YYYY)
216
- // If it matches our format patterns, return as-is
217
- if (typeof label === 'string') {
218
- // Check for MMM YY format (e.g., "Dec 24", "Jan 25")
219
- if (/^[A-Z][a-z]{2} \d{2}$/.test(label)) {
220
- return label;
221
- }
222
- // Check for DD/MM format (e.g., "03/11")
223
- if (/^\d{2}\/\d{2}$/.test(label)) {
224
- return label;
225
- }
226
- // Check for W# format (e.g., "W1", "W12")
227
- if (/^W\d+$/.test(label)) {
228
- return label;
229
- }
230
- // Check for YYYY format (e.g., "2024", "2025")
231
- if (/^\d{4}$/.test(label)) {
232
- return label;
233
- }
234
- }
235
-
236
- // Otherwise, try to parse and format it
237
- let date = dayjs(label);
238
-
239
- // If first attempt fails, try parsing as ISO date string
240
- if (!date.isValid() && typeof label === 'string') {
241
- date = dayjs(label, ['YYYY-MM-DD', 'YYYY-MM', 'YYYY', 'MMM YY', 'MMM YYYY', 'DD/MM'], true);
242
- }
243
-
244
- // If it's a valid date, format it using getFormatDate
245
- if (date.isValid() && getFormatDateFn) {
246
- return getFormatDateFn(date, false);
247
- }
248
-
249
- // Return as-is if we can't parse it
250
- return label;
229
+ if (!label) return label;
230
+
231
+ // Check if label is already in the correct format (MMM YY, DD/MM, W#, or YYYY)
232
+ // If it matches our format patterns, return as-is
233
+ if (typeof label === 'string') {
234
+ // Check for MMM YY format (e.g., "Dec 24", "Jan 25")
235
+ if (/^[A-Z][a-z]{2} \d{2}$/.test(label)) {
236
+ return label;
237
+ }
238
+ // Check for DD/MM format (e.g., "03/11")
239
+ if (/^\d{2}\/\d{2}$/.test(label)) {
240
+ return label;
241
+ }
242
+ // Check for W# format (e.g., "W1", "W12")
243
+ if (/^W\d+$/.test(label)) {
244
+ return label;
245
+ }
246
+ // Check for YYYY format (e.g., "2024", "2025")
247
+ if (/^\d{4}$/.test(label)) {
248
+ return label;
249
+ }
250
+ }
251
+
252
+ // Otherwise, try to parse and format it
253
+ let date = dayjs(label);
254
+
255
+ // If first attempt fails, try parsing as ISO date string
256
+ if (!date.isValid() && typeof label === 'string') {
257
+ date = dayjs(label, ['YYYY-MM-DD', 'YYYY-MM', 'YYYY', 'MMM YY', 'MMM YYYY', 'DD/MM'], true);
258
+ }
259
+
260
+ // If it's a valid date, format it using getFormatDate
261
+ if (date.isValid() && getFormatDateFn) {
262
+ return getFormatDateFn(date, false);
263
+ }
264
+
265
+ // Return as-is if we can't parse it
266
+ return label;
251
267
  };
252
268
 
269
+