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

michaelmbugua-me / DataAnalyticsPlatform / #14

29 Aug 2025 11:17AM UTC coverage: 26.207% (-0.6%) from 26.838%
#14

push

github

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

94 of 829 branches covered (11.34%)

Branch coverage included in aggregate %.

530 of 1552 relevant lines covered (34.15%)

1.81 hits per line

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

9.55
/src/app/features/AnalysisToolsModule/AnalysisToolsDaily/analysis-tools-daily.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 {PageHeaderComponent} from '../../shared/components/shared-globally/PageHeaderComponent/PageHeaderComponent';
1✔
9
import {PivotTableComponentComponent} from '../../shared/components/analysis/PivotTableComponent/pivot-table-component.component';
1✔
10
import {GroupedTableComponentComponent} from '../../shared/components/analysis/GroupedTableComponent/grouped-table-component.component';
1✔
11
import {AnalysisCardsComponentComponent} from '../../shared/components/analysis/AnalysisCardsComponent/analysis-cards-component.component';
1✔
12
import {
1✔
13
  GroupAndPivotDrawerComponent
14
} from '../../shared/components/analysis/GroupAndPivotDrawerComponent/group-and-pivot-drawer-component';
15
import {MenuItem} from 'primeng/api';
16
import {DataExportationService} from '../../../core/services/DataExportationService';
1✔
17

18

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

30
  groupColumnDefs: any[] = [];
1✔
31
  groupRowData: any[] = [];
1✔
32
  domLayout: 'autoHeight' = 'autoHeight';
1✔
33
  pivotColumnDefs: any[] = [];
1✔
34
  pivotRowData: any[] = [];
1✔
35
  dimensionOptions: { label: string; value: string }[] = [];
1✔
36
  measureOptions: { label: string; value: string }[] = [];
1✔
37
  visible = signal(false);
1✔
38
  groupTableLoaded: boolean = true;
1✔
39
  pivotTableLoaded: boolean = true;
1✔
40

41
  totalEvents = 0;
1✔
42
  averageDuration = '0ms';
1✔
43
  uniqueUsers = 0;
1✔
44

45
  items: MenuItem[] | undefined;
46
  pivotItems: MenuItem[] | undefined;
47
  private dataService = inject(DataService);
1✔
48
  public data = this.dataService.dailyRollups;
1✔
49
  loading = this.dataService.loading;
1✔
50
  error = this.dataService.error;
1✔
51
  private dataExportationService = inject(DataExportationService);
1✔
52

53

54
  private dataGroupFormValue: any;
55
  private pivotTableFormValue: any;
56

57
  constructor() {
58

59

60
    this.items = [{
1✔
61
      label: 'Export as PDF', command: () => {
62
        this.exportGroupedTable('pdf');
×
63
      }
64
    }, {
65
      label: 'Export as Excel', command: () => {
66
        this.exportGroupedTable('xlsx');
×
67
      }
68
    }, {
69
      label: 'Export as CSV', command: () => {
70
        this.exportGroupedTable('csv');
×
71
      }
72
    }]
73

74
    this.pivotItems = [{
1✔
75
      label: 'Export as PDF', command: () => {
76
        this.exportPivotTable('pdf');
×
77
      }
78
    }, {
79
      label: 'Export as Excel', command: () => {
80
        this.exportPivotTable('xlsx');
×
81
      }
82
    }, {
83
      label: 'Export as CSV', command: () => {
84
        this.exportPivotTable('csv');
×
85
      }
86
    }]
87

88
    effect(() => {
1✔
89
      const records = this.data();
×
90
      this.buildSelectOptions(records);
×
91
      if (records && records.length > 0) {
×
92
        this.fetchCardStatistics();
×
93
        this.groupData();
×
94
        this.createPivotTable();
×
95
      }
96
    });
97
  }
98

99
  onGroupSubmit(formValue: any) {
100
    this.dataGroupFormValue = formValue;
×
101
    this.groupData();
×
102
  }
103

104
  onPivotSubmit(formValue: any) {
105
    this.pivotTableFormValue = formValue;
×
106
    this.createPivotTable();
×
107
  }
108

109
  async ngOnInit() {
110

111
    if (this.data() && this.data().length > 0) {
×
112
      this.fetchCardStatistics();
×
113
      this.groupData();
×
114
      this.createPivotTable();
×
115
    }
116

117
  }
118

119
  groupData() {
120

121
    // this.groupTableLoaded = false;
122

123
    if (!this.dataGroupFormValue) {
×
124
      console.warn('Form values not initialized');
×
125
      return;
×
126
    }
127

128
    const {groupBy: dimension, aggregation, measure} = this.dataGroupFormValue;
×
129

130
    if (!dimension) {
×
131
      alert('Please select a dimension to group by');
×
132
      return;
×
133
    }
134

135
    const getDim = (it: any, key: string) => {
×
136
      if (key === 'event_name') {
×
137
        return (it?.event_name ?? it?.event_group ?? 'Unknown');
×
138
      }
139
      return (it?.[key] ?? 'Unknown');
×
140
    };
141

142
    const groupedData: Record<string, any[]> = {};
×
143

144
    this.data().forEach((item: any) => {
×
145
      const key = getDim(item, dimension);
×
146
      if (!groupedData[key]) {
×
147
        groupedData[key] = [];
×
148
      }
149
      groupedData[key].push(item);
×
150
    });
151

152
    const result: any[] = [];
×
153
    for (const key in groupedData) {
×
154
      const group = groupedData[key];
×
155
      let value: number = 0;
×
156

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

176
      result.push({
×
177
        [dimension]: key, value: Math.round(value * 100) / 100
178
      });
179
    }
180

181
    // Create grouped grid
182
    const columnDefs = [{headerName: dimension, field: dimension}, {
×
183
      headerName: String(aggregation).toUpperCase(), field: 'value'
184
    }];
185

186
    this.groupColumnDefs = [...columnDefs];
×
187
    this.groupRowData = [...result];
×
188
    this.groupTableLoaded = true;
×
189

190
  }
191

192
  createPivotTable() {
193

194
    if (!this.pivotTableFormValue) {
×
195
      console.warn('Form values not initialized');
×
196
      return;
×
197
    }
198
    const {rowDimension, columnDimension, valueMeasure} = this.pivotTableFormValue;
×
199

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

205
    const pivotData: any = {};
×
206
    const columnValues = new Set();
×
207

208
    const getDim = (it: any, key: string) => {
×
209
      if (!key) return 'Total';
×
210
      if (key === 'event_name') {
×
211
        return (it?.event_name ?? it?.event_group ?? 'Unknown');
×
212
      }
213
      return (it?.[key] ?? 'Unknown');
×
214
    };
215

216
    this.data().forEach((item: any) => {
×
217
      const rowValue = getDim(item, rowDimension);
×
218
      const colValue = columnDimension ? getDim(item, columnDimension) : 'Total';
×
219

220
      if (columnDimension) columnValues.add(colValue);
×
221

222
      if (!pivotData[rowValue]) {
×
223
        pivotData[rowValue] = {};
×
224
      }
225

226
      if (!pivotData[rowValue][colValue]) {
×
227
        pivotData[rowValue][colValue] = {count: 0, sum: 0};
×
228
      }
229

230
      pivotData[rowValue][colValue].count += 1;
×
231
      if (valueMeasure !== 'count') {
×
232
        pivotData[rowValue][colValue].sum += Number(item?.[valueMeasure]) || 0;
×
233
      }
234
    });
235

236
    const columnDefs: any = [{headerName: rowDimension, field: rowDimension, pinned: 'left'}];
×
237

238
    const columnArray: any = columnDimension ? Array.from(columnValues) : ['Total'];
×
239
    columnArray.forEach((col: string | number) => {
×
240
      columnDefs.push({
×
241
        headerName: col, valueGetter: (params: { data: { [x: string]: any; }; }) => {
242
          const rowValue = params.data[rowDimension];
×
243
          const cellData = pivotData[rowValue] && pivotData[rowValue][col];
×
244
          return cellData ? (valueMeasure === 'count' ? cellData.count : cellData.sum) : 0;
×
245
        }
246
      });
247
    });
248

249
    if (columnDimension) {
×
250
      columnDefs.push({
×
251
        headerName: 'Total', valueGetter: (params: { data: { [x: string]: any; }; }) => {
252
          const rowValue = params.data[rowDimension];
×
253
          let total = 0;
×
254
          for (const col in pivotData[rowValue]) {
×
255
            total += valueMeasure === 'count' ? pivotData[rowValue][col].count : pivotData[rowValue][col].sum;
×
256
          }
257
          return total;
×
258
        }
259
      });
260
    }
261

262
    const rowData = Object.keys(pivotData).map(rowValue => {
×
263
      return {[rowDimension]: rowValue};
×
264
    });
265

266

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

271

272
  }
273

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

278
  exportPivotTable(type: 'pdf' | 'xlsx' | 'csv' = 'pdf') {
×
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 ''
×
285
      }
286
    });
287

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

290
    const view = this.pivotRowData;
×
291

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

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

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

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

325
  exportGroupedTable(type: 'pdf' | 'xlsx' | 'csv' = 'pdf') {
×
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 ''
×
332
      }
333
    });
334

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

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

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

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

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

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
    }
369
  }
370

371

372
  private buildSelectOptions(records: any[]) {
373
    const defaultDims = ['day', 'source', 'platform', 'app_id', 'app_version', 'release_channel', 'country', 'device_tier', 'event_group'];
×
374
    const first = records?.[0] ?? {};
×
375
    const keys = Object.keys(first || {});
×
376

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

379
    const withAlias = [...dims];
×
380
    if (!('event_name' in first) && ('event_group' in first)) {
×
381
      withAlias.push('event_name');
×
382
    }
383

384
    // Numeric measures present in daily rollups
385
    const numericCandidates = ['events_count', 'users_count', 'sessions_count', 'avg_duration_ms', 'p50_duration_ms', 'p90_duration_ms', 'p99_duration_ms', 'http_error_rate', 'crash_rate_per_1k_sessions', 'revenue_usd', 'purchase_count'];
×
386
    const measures = numericCandidates.filter(k => k in first);
×
387

388
    this.dimensionOptions = [{
×
389
      label: '-- Select Dimension --', value: ''
390
    }, ...withAlias.map(k => ({label: k === 'event_name' ? 'Event Type' : this.pretty(k), value: k}))];
×
391

392
    this.measureOptions = measures.length ? measures.map(k => ({
×
393
      label: this.pretty(k), value: k
394
    })) : [{label: 'Events Count', value: 'events_count'}];
395
  }
396

397
  private pretty(key: string) {
398
    return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
×
399
  }
400

401
  private fetchCardStatistics() {
402

403
    const data = this.data();
×
404

405
    console.log(data.length)
×
406

407
    const totalEvents = data.reduce((acc: number, r: any) => acc + (Number(r?.events_count) || 0), 0);
×
408
    const totalUsers = data.reduce((acc: number, r: any) => acc + (Number(r?.users_count) || 0), 0);
×
409

410
    this.totalEvents = totalEvents;
×
411
    this.uniqueUsers = totalUsers;
×
412

413
    if (!data.length) {
×
414
      this.averageDuration = '0.00 ms';
×
415
      return;
×
416
    }
417

418
    const durations = data
×
419
      .map((r: any) => Number(r?.avg_duration_ms))
×
420
      .filter((v: number) => !isNaN(v));
×
421

422
    if (durations.length === 0) {
×
423
      this.averageDuration = '0.00 ms';
×
424
      return;
×
425
    }
426

427
    const avg = durations.reduce((a: number, b: number) => a + b, 0) / durations.length;
×
428
    this.averageDuration = avg.toFixed(2) + ' ms';
×
429
  }
430

431
}
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