• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

michaelmbugua-me / DataAnalyticsPlatform / #17

29 Aug 2025 11:42AM UTC coverage: 32.129% (+5.9%) from 26.207%
#17

push

github

Michael Mbugua
Refactor code to remove duplication (Analysis and visualization)

131 of 829 branches covered (15.8%)

Branch coverage included in aggregate %.

634 of 1552 relevant lines covered (40.85%)

2.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.15
/src/app/features/AnalysisToolsModule/AnalysisToolsRaw/analysis-tools-raw.component.ts
1
import {ChangeDetectionStrategy, Component, effect, inject, OnInit, signal} from '@angular/core';
1✔
2
import {TableModule} from 'primeng/table';
1✔
3

4
import {ProgressSpinner} from 'primeng/progressspinner';
1✔
5
import {ButtonDirective, ButtonIcon, ButtonLabel} from 'primeng/button';
1✔
6
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
1✔
7
import {DataService} from '../../../core/services/DataService';
1✔
8
import {AllCommunityModule, ModuleRegistry} from 'ag-grid-community';
1✔
9
import {PageHeaderComponent} from '../../shared/components/shared-globally/PageHeaderComponent/PageHeaderComponent';
1✔
10
import {DataExportationService} from '../../../core/services/DataExportationService';
1✔
11
import {MenuItem} from 'primeng/api';
12
import {PivotTableComponentComponent} from '../../shared/components/analysis/PivotTableComponent/pivot-table-component.component';
1✔
13
import {GroupedTableComponentComponent} from '../../shared/components/analysis/GroupedTableComponent/grouped-table-component.component';
1✔
14
import {AnalysisCardsComponentComponent} from '../../shared/components/analysis/AnalysisCardsComponent/analysis-cards-component.component';
1✔
15
import {
1✔
16
  GroupAndPivotDrawerComponent
17
} from '../../shared/components/analysis/GroupAndPivotDrawerComponent/group-and-pivot-drawer-component';
18

19
ModuleRegistry.registerModules([AllCommunityModule]);
1✔
20

21

22
@Component({
23
  selector: 'app-analysis-tools',
24
  templateUrl: './analysis-tools-raw.component.html',
25
  imports: [TableModule, ProgressSpinner, ButtonDirective, ButtonIcon, ButtonLabel, ReactiveFormsModule, FormsModule, PageHeaderComponent, PivotTableComponentComponent, GroupedTableComponentComponent, AnalysisCardsComponentComponent, GroupAndPivotDrawerComponent,],
26
  providers: [],
27
  standalone: true,
28
  changeDetection: ChangeDetectionStrategy.OnPush,
29
  styleUrls: ['./analysis-tools-raw.component.scss']
30
})
31
export class AnalysisToolsRawComponent implements OnInit {
1✔
32

33
  groupColumnDefs: any[] = [];
13✔
34
  groupRowData: any[] = [];
13✔
35
  domLayout: 'autoHeight' = 'autoHeight';
13✔
36
  pivotColumnDefs: any[] = [];
13✔
37
  pivotRowData: any[] = [];
13✔
38
  filters!: Filter[];
39
  dimensionOptions: { label: string; value: string }[] = [];
13✔
40
  measureOptions: { label: string; value: string }[] = [];
13✔
41
  visible = signal(false);
13✔
42
  groupTableLoaded: boolean = true;
13✔
43
  pivotTableLoaded: boolean = true;
13✔
44

45
  totalEvents = 0;
13✔
46
  averageDuration = '0ms';
13✔
47
  uniqueUsers = 0;
13✔
48

49
  items: MenuItem[] | undefined;
50
  pivotItems: MenuItem[] | undefined;
51
  private dataService = inject(DataService);
13✔
52
  public data = this.dataService.data;
13✔
53
  loading = this.dataService.loading;
13✔
54
  error = this.dataService.error;
13✔
55
  private dataExportationService = inject(DataExportationService);
13✔
56

57

58
  private dataGroupFormValue: any;
59
  private pivotTableFormValue: any;
60

61
  constructor() {
62

63

64
    this.items = [{
13✔
65
      label: 'Export as PDF', command: () => {
66
        this.exportGroupedTable('pdf');
×
67
      }
68
    }, {
69
      label: 'Export as Excel', command: () => {
70
        this.exportGroupedTable('xlsx');
×
71
      }
72
    }, {
73
      label: 'Export as CSV', command: () => {
74
        this.exportGroupedTable('csv');
×
75
      }
76
    }]
77

78
    this.pivotItems = [{
13✔
79
      label: 'Export as PDF', command: () => {
80
        this.exportPivotTable('pdf');
×
81
      }
82
    }, {
83
      label: 'Export as Excel', command: () => {
84
        this.exportPivotTable('xlsx');
×
85
      }
86
    }, {
87
      label: 'Export as CSV', command: () => {
88
        this.exportPivotTable('csv');
×
89
      }
90
    }]
91

92
    effect(() => {
13✔
93
      const records = this.data();
×
94
      this.buildSelectOptions(records);
×
95
      if (records && records.length > 0) {
×
96
        this.fetchCardStatistics();
×
97
        this.groupData();
×
98
        this.createPivotTable();
×
99
      }
100
    });
101
  }
102

103
  onGroupSubmit(formValue: any) {
104
    this.dataGroupFormValue = formValue;
1✔
105
    this.groupData();
1✔
106
  }
107

108
  onPivotSubmit(formValue: any) {
109
    this.pivotTableFormValue = formValue;
1✔
110
    this.createPivotTable();
1✔
111
  }
112

113
  async ngOnInit() {
114

115
    if (this.data() && this.data().length > 0) {
×
116
      this.fetchCardStatistics();
×
117
      this.groupData();
×
118
      this.createPivotTable();
×
119
    }
120
  }
121

122
  groupData() {
123

124
    // this.groupTableLoaded = false;
125

126
    if (!this.dataGroupFormValue) {
1!
127
      console.warn('Form values not initialized');
×
128
      return;
×
129
    }
130
      const {groupBy: dimension, aggregation, measure} = this.dataGroupFormValue;
1✔
131

132

133
    if (!dimension) {
1!
134
      alert('Please select a dimension to group by');
×
135
      return;
×
136
    }
137

138
    const groupedData: Record<string, any[]> = {};
1✔
139

140
    this.data().forEach((item: any) => {
1✔
141
      const key = item?.[dimension] ?? 'Unknown';
×
142
      if (!groupedData[key]) {
×
143
        groupedData[key] = [];
×
144
      }
145
      groupedData[key].push(item);
×
146
    });
147

148
    // Apply aggregation
149
    const result: any[] = [];
1✔
150
    for (const key in groupedData) {
1✔
151
      const group = groupedData[key];
×
152
      let value: number = 0;
×
153

154
      switch (aggregation) {
×
155
        case 'count':
156
          value = group.length;
×
157
          break;
×
158
        case 'sum':
159
          value = group.reduce((sum: number, item: any) => sum + (Number(item?.[measure]) || 0), 0);
×
160
          break;
×
161
        case 'avg':
162
          const sum = group.reduce((total: number, item: any) => total + (Number(item?.[measure]) || 0), 0);
×
163
          value = group.length ? sum / group.length : 0;
×
164
          break;
×
165
        case 'min':
166
          value = Math.min(...group.map((item: any) => Number(item?.[measure]) || 0));
×
167
          break;
×
168
        case 'max':
169
          value = Math.max(...group.map((item: any) => Number(item?.[measure]) || 0));
×
170
          break;
×
171
      }
172

173
      result.push({
×
174
        [dimension]: key, value: Math.round(value * 100) / 100
175
      });
176
    }
177

178
    // Create grouped grid
179
    const columnDefs = [{headerName: dimension, field: dimension}, {
1✔
180
      headerName: String(aggregation).toUpperCase(), field: 'value'
181
    }];
182

183
    this.groupColumnDefs = [...columnDefs];
1✔
184
    this.groupRowData = [...result];
1✔
185
    this.groupTableLoaded = true;
1✔
186

187
  }
188

189

190
  createPivotTable() {
191

192
    if (!this.pivotTableFormValue) {
1!
193
      console.warn('Form values not initialized');
×
194
      return;
×
195
    }
196

197
    const {rowDimension, columnDimension, valueMeasure} = this.pivotTableFormValue;
1✔
198

199

200
    if (!rowDimension) {
1!
201
      alert('Please select at least a row dimension');
×
202
      return;
×
203
    }
204

205
    // Group data by row and column dimensions
206
    const pivotData: any = {};
1✔
207
    const columnValues = new Set();
1✔
208

209
    this.data().forEach((item: any) => {
1✔
210
      const rowValue = item[rowDimension] || 'Unknown';
×
211
      const colValue = columnDimension ? (item[columnDimension] || 'Unknown') : 'Total';
×
212

213
      // Add to column values set
214
      if (columnDimension) columnValues.add(colValue);
×
215

216
      if (!pivotData[rowValue]) {
×
217
        pivotData[rowValue] = {};
×
218
      }
219

220
      if (!pivotData[rowValue][colValue]) {
×
221
        pivotData[rowValue][colValue] = {count: 0, sum: 0};
×
222
      }
223

224
      // Update measures
×
225
      pivotData[rowValue][colValue].count += 1;
×
226
      // Only accumulate sum when valueMeasure is not 'count'
×
227
      if (valueMeasure !== 'count') {
228
        pivotData[rowValue][colValue].sum += Number(item?.[valueMeasure]) || 0;
229
      }
230
    });
1✔
231

232
    // Prepare column definitions
1!
233
    const columnDefs: any = [{headerName: rowDimension, field: rowDimension, pinned: 'left'}];
1✔
234

×
235
    // Add column dimension values as columns
236
    const columnArray: any = columnDimension ? Array.from(columnValues) : ['Total'];
×
237
    columnArray.forEach((col: string | number) => {
×
238
      columnDefs.push({
×
239
        headerName: col, valueGetter: (params: { data: { [x: string]: any; }; }) => {
240
          const rowValue = params.data[rowDimension];
241
          const cellData = pivotData[rowValue] && pivotData[rowValue][col];
242
          return cellData ? (valueMeasure === 'count' ? cellData.count : cellData.sum) : 0;
243
        }
244
      });
1!
245
    });
1✔
246

247
    // Add total column only when there is a column dimension (to provide row totals)
×
248
    if (columnDimension) {
×
249
      columnDefs.push({
×
250
        headerName: 'Total', valueGetter: (params: { data: { [x: string]: any; }; }) => {
×
251
          const rowValue = params.data[rowDimension];
252
          let total = 0;
×
253
          for (const col in pivotData[rowValue]) {
254
            total += valueMeasure === 'count' ? pivotData[rowValue][col].count : pivotData[rowValue][col].sum;
255
          }
256
          return total;
257
        }
258
      });
1✔
259
    }
×
260

261
    // Prepare row data
262
    const rowData = Object.keys(pivotData).map(rowValue => {
263
      return {[rowDimension]: rowValue};
1✔
264
    });
1✔
265

1✔
266

267
    this.pivotColumnDefs = [...columnDefs];
268
    this.pivotRowData = [...rowData];
269
    this.pivotTableLoaded = true;
270

271

2✔
272
  }
273

274
  toggleFilterVisibility() {
×
275
    this.visible.update(v => !v);
276
  }
2✔
277

1!
278
  exportPivotTable(type: 'pdf' | 'xlsx' | 'csv' = 'pdf') {
1!
279
    // Build column headers and row data from current grid data
280
    let cols: string[] = this.pivotColumnDefs.map((item: any) => {
×
281
      if (item['headerName'] && String(item['headerName']).toLowerCase() !== 'actions') {
282
        return String(item['field'] ?? '').toUpperCase();
283
      } else {
284
        return ''
2✔
285
      }
286
    });
2✔
287

288
    cols = cols.filter(item => item !== '' && item !== undefined);
2✔
289

290
    const view = this.pivotRowData;
1✔
291

292
    if (!view || view.length === 0) return;
293

1✔
294
    const rowKeys: string[] = Object.keys(view[0] as any);
295

1✔
296
    // rows for PDF/Excel (matrix)
297
    const matrix: (string | number)[][] = [];
1✔
298
    // rows for CSV/XLSX JSON form
1✔
299
    const jsonRows: Record<string, any>[] = [];
1✔
300

1✔
301
    view.forEach((row: any) => {
1✔
302
      const temp: (string | number)[] = [];
1!
303
      const jsonRow: Record<string, any> = {};
1✔
304
      cols.forEach(colKey => {
1✔
305
        rowKeys.forEach(key => {
306
          if (colKey === key.toUpperCase()) {
307
            temp.push(row[key]);
308
            jsonRow[key] = row[key];
1✔
309
          }
1✔
310
        })
311
      })
312
      matrix.push(temp);
1!
313
      jsonRows.push(jsonRow);
1✔
314
    })
×
315

×
316
    if (type === 'pdf') {
×
317
      this.dataExportationService.exportToPdf(cols, matrix as string[][], 'Pivot_Table_Data.pdf');
×
318
    } else if (type === 'xlsx') {
319
      this.dataExportationService.exportDataXlsx(jsonRows, 'Pivot_Table_Data');
320
    } else if (type === 'csv') {
321
      this.dataExportationService.exportToCsv(jsonRows as any, 'Pivot_Table_Data');
×
322
    }
323
  }
1✔
324

1!
325
  exportGroupedTable(type: 'pdf' | 'xlsx' | 'csv' = 'pdf') {
1!
326
    // Build column headers and row data from current grid data
327
    let cols: string[] = this.groupColumnDefs.map((item: any) => {
×
328
      if (item['headerName'] && String(item['headerName']).toLowerCase() !== 'actions') {
329
        return String(item['field'] ?? '').toUpperCase();
330
      } else {
331
        return ''
1✔
332
      }
333
    });
1✔
334

1!
335
    cols = cols.filter(item => item !== '' && item !== undefined);
336

1✔
337
    const view = this.groupRowData;
338
    if (!view || view.length === 0) return;
339

1✔
340
    const rowKeys: string[] = Object.keys(view[0] as any);
341

1✔
342
    // rows for PDF/Excel (matrix)
343
    const matrix: (string | number)[][] = [];
1✔
344
    // rows for CSV/XLSX JSON form
1✔
345
    const jsonRows: Record<string, any>[] = [];
1✔
346

1✔
347
    view.forEach((row: any) => {
1✔
348
      const temp: (string | number)[] = [];
1!
349
      const jsonRow: Record<string, any> = {};
1✔
350
      cols.forEach(colKey => {
1✔
351
        rowKeys.forEach(key => {
352
          if (colKey === key.toUpperCase()) {
353
            temp.push(row[key]);
354
            jsonRow[key] = row[key];
1✔
355
          }
1✔
356
        })
357
      })
358
      matrix.push(temp);
1!
359
      jsonRows.push(jsonRow);
×
360
    })
1!
361

1✔
362
    if (type === 'pdf') {
×
363
      this.dataExportationService.exportToPdf(cols, matrix as string[][], 'Grouped_Table.pdf');
×
364
    } else if (type === 'xlsx') {
365
      this.dataExportationService.exportDataXlsx(jsonRows, 'Grouped_Table');
366
    } else if (type === 'csv') {
367
      this.dataExportationService.exportToCsv(jsonRows as any, 'Grouped_Table');
368
    }
2✔
369
  }
2✔
370

2!
371
  private buildSelectOptions(records: any[]) {
372
    const defaultDims = ['platform', 'country', 'device_tier', 'event_name', 'release_channel', 'source', 'day', 'app_version', 'network_type'];
18✔
373
    const first = records?.[0] ?? {};
374
    const keys = Object.keys(first || {});
375

2✔
376
    const dims = defaultDims.filter(k => k in first).concat(keys.filter(k => typeof first[k] === 'string' && !defaultDims.includes(k)));
8✔
377

378
    // Numeric measures present in data
2✔
379
    const numericCandidates = ['count', 'duration_ms', 'revenue_usd', 'purchase_count'];
380
    const measures = numericCandidates.filter(k => k in first);
381

382
    this.dimensionOptions = [{label: '-- Select Dimension --', value: ''}, ...dims.map(k => ({
2✔
383
      label: this.pretty(k), value: k
384
    }))];
1✔
385

386
    this.measureOptions = [{
387
      label: 'Count', value: 'count'
388
    }, ...measures.filter(m => m !== 'count').map(k => ({label: this.pretty(k), value: k}))];
6✔
389
  }
390

391
  private pretty(key: string) {
392
    return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
393
  }
2✔
394

2✔
395
  private fetchCardStatistics() {
396

2!
397
    const data = this.data();
2✔
398
    this.totalEvents = data.length;
2✔
399

2✔
400
    if (this.totalEvents === 0) {
401
      this.averageDuration = '0.00 ms';
402
      this.uniqueUsers = 0;
×
403
      return;
×
404
    }
405

×
406
    let totalDuration = 0;
×
407
    const uniqueUserIds = new Set();
×
408

×
409
    data.forEach((event: any) => {
410
      totalDuration += event.duration_ms || 0;
411
      if (event.user_pseudo_id) {
412
        uniqueUserIds.add(event.user_pseudo_id);
×
413
      }
×
414
    });
415

416
    this.averageDuration = (totalDuration / this.totalEvents).toFixed(2) + ' ms';
417
    this.uniqueUsers = uniqueUserIds.size;
418

419
  }
420
}
421

422
interface Filter {
423
  name: string,
424
  code: string
425
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc