Skip to content

Commit b708028

Browse files
authored
fix heatmap (#690)
- **fixes for heatmap data munging** - **fix timestamp parsing** Closes #660
2 parents b38615d + b126bf6 commit b708028

File tree

3 files changed

+212
-22
lines changed

3 files changed

+212
-22
lines changed

packages/common-ui/src/components/HeatMap.vue

Lines changed: 134 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ export default defineComponent({
8989
return 7 * (this.cellSize + this.cellMargin);
9090
},
9191
effectiveActivityRecords(): ActivityRecord[] {
92-
return this.localActivityRecords.length > 0 ? this.localActivityRecords : this.activityRecords;
92+
const useLocal = Array.isArray(this.localActivityRecords) && this.localActivityRecords.length > 0;
93+
const records = useLocal ? this.localActivityRecords : this.activityRecords || [];
94+
console.log('Using effectiveActivityRecords, count:', records.length, 'source:', useLocal ? 'local' : 'prop');
95+
return records;
9396
},
9497
},
9598
@@ -107,12 +110,43 @@ export default defineComponent({
107110
if (this.activityRecordsGetter) {
108111
try {
109112
this.isLoading = true;
110-
this.localActivityRecords = await this.activityRecordsGetter();
113+
console.log('Fetching activity records using getter...');
114+
115+
// Ensure the getter is called safely with proper error handling
116+
let result = await this.activityRecordsGetter();
117+
118+
// Handle the result - ensure it's an array of activity records
119+
if (Array.isArray(result)) {
120+
// Filter out records with invalid timestamps before processing
121+
this.localActivityRecords = result.filter(record => {
122+
if (!record || !record.timeStamp) return false;
123+
124+
// Basic validation check for timestamps
125+
try {
126+
const m = moment(record.timeStamp);
127+
return m.isValid() && m.year() > 2000 && m.year() < 2100;
128+
} catch (e) {
129+
return false;
130+
}
131+
});
132+
133+
console.log(`Received ${result.length} records, ${this.localActivityRecords.length} valid after filtering`);
134+
135+
// Process the loaded records
136+
this.processRecords();
137+
this.createWeeksData();
138+
} else {
139+
console.error('Activity records getter did not return an array:', result);
140+
this.localActivityRecords = [];
141+
}
111142
} catch (error) {
112143
console.error('Error fetching activity records:', error);
144+
this.localActivityRecords = [];
113145
} finally {
114146
this.isLoading = false;
115147
}
148+
} else {
149+
console.log('No activityRecordsGetter provided, using direct activityRecords prop');
116150
}
117151
},
118152
@@ -123,15 +157,80 @@ export default defineComponent({
123157
},
124158
125159
processRecords() {
126-
const records = this.effectiveActivityRecords;
160+
const records = this.effectiveActivityRecords || [];
127161
console.log(`Processing ${records.length} records`);
128162
129163
const data: { [key: string]: number } = {};
130164
131-
records.forEach((record) => {
132-
const date = moment(record.timeStamp).format('YYYY-MM-DD');
133-
data[date] = (data[date] || 0) + 1;
134-
});
165+
if (records.length === 0) {
166+
console.log('No records to process');
167+
this.heatmapData = data;
168+
return;
169+
}
170+
171+
// Sample logging of a few records to understand structure without flooding console
172+
const uniqueDates = new Set<string>();
173+
const dateDistribution: Record<string, number> = {};
174+
let validCount = 0;
175+
let invalidCount = 0;
176+
177+
for (let i = 0; i < records.length; i++) {
178+
const record = records[i];
179+
180+
if (!record || typeof record !== 'object' || !record.timeStamp) {
181+
invalidCount++;
182+
continue;
183+
}
184+
185+
try {
186+
// Attempt to normalize the timestamp
187+
let normalizedDate: string;
188+
189+
if (typeof record.timeStamp === 'string') {
190+
// For ISO strings, parse directly with moment
191+
normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');
192+
} else if (typeof record.timeStamp === 'number') {
193+
// For numeric timestamps, use Date constructor then moment
194+
normalizedDate = moment(new Date(record.timeStamp)).format('YYYY-MM-DD');
195+
} else if (typeof record.timeStamp === 'object') {
196+
// For objects (like Moment), try toString() or direct parsing
197+
if (typeof record.timeStamp.format === 'function') {
198+
// It's likely a Moment object
199+
normalizedDate = record.timeStamp.format('YYYY-MM-DD');
200+
} else if (record.timeStamp instanceof Date) {
201+
normalizedDate = moment(record.timeStamp).format('YYYY-MM-DD');
202+
} else {
203+
// Try to parse it as a string representation
204+
normalizedDate = moment(String(record.timeStamp)).format('YYYY-MM-DD');
205+
}
206+
} else {
207+
// Unhandled type
208+
invalidCount++;
209+
continue;
210+
}
211+
212+
// Verify the date is valid before using it
213+
if (moment(normalizedDate, 'YYYY-MM-DD', true).isValid()) {
214+
data[normalizedDate] = (data[normalizedDate] || 0) + 1;
215+
uniqueDates.add(normalizedDate);
216+
217+
// Track distribution by month for debugging
218+
const month = normalizedDate.substring(0, 7); // YYYY-MM
219+
dateDistribution[month] = (dateDistribution[month] || 0) + 1;
220+
221+
validCount++;
222+
} else {
223+
invalidCount++;
224+
}
225+
} catch (e) {
226+
invalidCount++;
227+
}
228+
}
229+
230+
// Log summary statistics
231+
console.log(`Processed ${validCount} valid dates, ${invalidCount} invalid dates`);
232+
console.log(`Found ${uniqueDates.size} unique dates`);
233+
console.log('Date distribution by month:', dateDistribution);
135234
136235
this.heatmapData = data;
137236
},
@@ -140,18 +239,31 @@ export default defineComponent({
140239
// Reset weeks and max count
141240
this.weeks = [];
142241
this.maxInRange = 0;
143-
242+
144243
const end = moment();
145244
const start = end.clone().subtract(52, 'weeks');
146245
const day = start.clone().startOf('week');
246+
247+
console.log('Creating weeks data from', start.format('YYYY-MM-DD'), 'to', end.format('YYYY-MM-DD'));
248+
249+
// Ensure we have data to display
250+
if (Object.keys(this.heatmapData).length === 0) {
251+
console.log('No heatmap data available to display');
252+
}
147253
254+
// For debugging, log some sample dates from the heatmap data
255+
const sampleDates = Object.keys(this.heatmapData).slice(0, 5);
256+
console.log('Sample dates in heatmap data:', sampleDates);
257+
258+
// Build the week data structure
148259
while (day.isSameOrBefore(end)) {
149260
const weekData: DayData[] = [];
150261
for (let i = 0; i < 7; i++) {
151262
const date = day.format('YYYY-MM-DD');
263+
const count = this.heatmapData[date] || 0;
152264
const dayData: DayData = {
153265
date,
154-
count: this.heatmapData[date] || 0,
266+
count,
155267
};
156268
weekData.push(dayData);
157269
if (dayData.count > this.maxInRange) {
@@ -162,6 +274,19 @@ export default defineComponent({
162274
}
163275
this.weeks.push(weekData);
164276
}
277+
278+
console.log('Weeks data created, maxInRange:', this.maxInRange);
279+
280+
// Calculate summary stats for display
281+
let totalDaysWithActivity = 0;
282+
let totalActivity = 0;
283+
284+
Object.values(this.heatmapData).forEach(count => {
285+
totalDaysWithActivity++;
286+
totalActivity += count;
287+
});
288+
289+
console.log(`Activity summary: ${totalActivity} activities across ${totalDaysWithActivity} days`);
165290
},
166291
167292
getColor(count: number): string {

packages/common-ui/src/components/StudySession.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
Start <a @click="$emit('session-finished')">another study session</a>, or try
2020
<router-link :to="`/edit/${courseID}`">adding some new content</router-link> to challenge yourself and others!
2121
</p>
22-
<heat-map :activity-records-getter="user.getActivityRecords" />
22+
<heat-map :activity-records-getter="() => user.getActivityRecords()" />
2323
</div>
2424

2525
<div v-else ref="shadowWrapper">

packages/db/src/impl/pouch/userDB.ts

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -245,20 +245,85 @@ Currently logged-in as ${this._username}.`
245245
}
246246

247247
public async getActivityRecords(): Promise<ActivityRecord[]> {
248-
const hist = await this.getHistory();
249-
250-
const allRecords: ActivityRecord[] = [];
251-
for (let i = 0; i < hist.length; i++) {
252-
if (hist[i] && hist[i]!.records) {
253-
hist[i]!.records.forEach((record: CardRecord) => {
254-
allRecords.push({
255-
timeStamp: record.timeStamp.toString(),
256-
});
257-
});
248+
try {
249+
const hist = await this.getHistory();
250+
251+
const allRecords: ActivityRecord[] = [];
252+
if (!Array.isArray(hist)) {
253+
console.error('getHistory did not return an array:', hist);
254+
return allRecords;
255+
}
256+
257+
// Sample the first few records to understand structure
258+
let sampleCount = 0;
259+
260+
for (let i = 0; i < hist.length; i++) {
261+
try {
262+
if (hist[i] && Array.isArray(hist[i]!.records)) {
263+
hist[i]!.records.forEach((record: CardRecord) => {
264+
try {
265+
// Skip this record if timeStamp is missing
266+
if (!record.timeStamp) {
267+
return;
268+
}
269+
270+
let timeStamp;
271+
272+
// Handle different timestamp formats
273+
if (typeof record.timeStamp === 'object') {
274+
// It's likely a Moment object
275+
if (typeof record.timeStamp.toDate === 'function') {
276+
// It's definitely a Moment object
277+
timeStamp = record.timeStamp.toISOString();
278+
} else if (record.timeStamp instanceof Date) {
279+
// It's a Date object
280+
timeStamp = record.timeStamp.toISOString();
281+
} else {
282+
// Log a sample of unknown object types, but don't flood console
283+
if (sampleCount < 3) {
284+
console.warn('Unknown timestamp object type:', record.timeStamp);
285+
sampleCount++;
286+
}
287+
return;
288+
}
289+
} else if (typeof record.timeStamp === 'string') {
290+
// It's already a string, but make sure it's a valid date
291+
const date = new Date(record.timeStamp);
292+
if (isNaN(date.getTime())) {
293+
return; // Invalid date string
294+
}
295+
timeStamp = record.timeStamp;
296+
} else if (typeof record.timeStamp === 'number') {
297+
// Assume it's a Unix timestamp (milliseconds since epoch)
298+
timeStamp = new Date(record.timeStamp).toISOString();
299+
} else {
300+
// Unknown type, skip
301+
return;
302+
}
303+
304+
allRecords.push({
305+
timeStamp,
306+
courseID: record.courseID || 'unknown',
307+
cardID: record.cardID || 'unknown',
308+
timeSpent: record.timeSpent || 0,
309+
type: 'card_view'
310+
});
311+
} catch (err) {
312+
// Silently skip problematic records to avoid flooding logs
313+
}
314+
});
315+
}
316+
} catch (err) {
317+
console.error('Error processing history item:', err);
318+
}
258319
}
259-
}
260320

261-
return allRecords;
321+
console.log(`Found ${allRecords.length} activity records`);
322+
return allRecords;
323+
} catch (err) {
324+
console.error('Error in getActivityRecords:', err);
325+
return [];
326+
}
262327
}
263328

264329
private async getReviewstoDate(targetDate: Moment, course_id?: string) {

0 commit comments

Comments
 (0)