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

michaelmbugua-me / DataAnalyticsPlatform / #5

26 Aug 2025 06:25PM UTC coverage: 20.788% (+0.6%) from 20.144%
#5

push

github

michaelmbugua-me
Add a data exportation service that allows exporting excel and pdf

62 of 502 branches covered (12.35%)

Branch coverage included in aggregate %.

6 of 73 new or added lines in 1 file covered. (8.22%)

479 existing lines in 7 files now uncovered.

265 of 1071 relevant lines covered (24.74%)

1.75 hits per line

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

4.21
/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 {Select} from 'primeng/select';
1✔
7
import {AgGridAngular} from 'ag-grid-angular';
1✔
8
import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms';
1✔
9
import {Drawer} from 'primeng/drawer';
1✔
10
import {DataService} from '../../../core/services/DataService';
1✔
11

12

13
@Component({
14
  selector: 'app-analysis-tools-daily',
15
  templateUrl: './analysis-tools-daily.component.html',
16
  imports: [
17
    TableModule,
18
    ProgressSpinner,
19
    ButtonDirective,
20
    ButtonIcon,
21
    ButtonLabel,
22
    Select,
23
    AgGridAngular,
24
    ReactiveFormsModule,
25
    Drawer,
26
    FormsModule,
27
  ],
28
  providers: [],
29
  standalone: true,
30
  changeDetection: ChangeDetectionStrategy.OnPush,
31
  styleUrls: ['./analysis-tools-daily.component.scss']
32
})
33
export class AnalysisToolsDailyComponent implements OnInit {
1✔
34

35
  private dataService = inject(DataService);
1✔
36

37
  public data = this.dataService.dailyRollups;
×
UNCOV
38
  loading = this.dataService.loading;
×
39
  error = this.dataService.error;
×
40

41
  groupColumnDefs: any[] = [];
×
42
  groupRowData: any[] = [];
×
UNCOV
43
  groupDefaultColDef = { sortable: true, filter: true, resizable: true };
×
44
  domLayout: 'autoHeight' = 'autoHeight';
×
45

46
  pivotColumnDefs: any[] = [];
×
UNCOV
47
  pivotRowData: any[] = [];
×
UNCOV
48
  pivotDefaultColDef = { sortable: true, filter: true, resizable: true };
×
49

50

51
  filters!: Filter[];
52

53
  // Dynamic select options derived from data
UNCOV
54
  dimensionOptions: { label: string; value: string }[] = [];
×
UNCOV
55
  measureOptions: { label: string; value: string }[] = [];
×
56

57
  visible = signal(false);
×
58

59
  dataGroupForm!: FormGroup;
60
  pivotTableForm!: FormGroup;
61

62

UNCOV
63
  groupTableLoaded: boolean = true;
×
UNCOV
64
  pivotTableLoaded: boolean = true;
×
65

66

67
  // Card Statistics
UNCOV
68
  totalEvents = 0;
×
UNCOV
69
  averageDuration = '0ms';
×
UNCOV
70
  uniqueUsers = 0;
×
71
  // Card Statistics
72

UNCOV
73
  constructor(private fb: FormBuilder) {
×
UNCOV
74
    this.dataGroupForm = fb.group({
×
75
      groupBy: ['country', Validators.required],
76
      aggregation: ['count', Validators.required],
77
      measure: ['events_count', Validators.required]
78
    });
79

UNCOV
80
    this.pivotTableForm = fb.group({
×
81
      rowDimension: ['platform', Validators.required],
82
      columnDimension: [''],
83
      valueMeasure: ['events_count', Validators.required]
84
    });
85

86
    // React to data arrivals/changes to build options and initialize tables
87
    effect(() => {
×
88
      const records = this.data();
×
UNCOV
89
      this.buildSelectOptions(records);
×
90
      if (records && records.length > 0) {
×
UNCOV
91
        this.fetchCardStatistics();
×
UNCOV
92
        this.groupData();
×
UNCOV
93
        this.createPivotTable();
×
94
      }
95
    });
96
  }
97

98
  private buildSelectOptions(records: any[]) {
99
    const defaultDims = ['day','source','platform','app_id','app_version','release_channel','country','device_tier','event_group'];
×
100
    const first = records?.[0] ?? {};
×
UNCOV
101
    const keys = Object.keys(first || {});
×
102

103
    const dims = defaultDims.filter(k => k in first).concat(
×
UNCOV
104
      keys.filter(k => typeof first[k] === 'string' && !defaultDims.includes(k))
×
105
    );
106

107
    // Inject a user-friendly alias: Event Type -> event_name (resolved later to event_group if needed)
108
    const withAlias = [...dims];
×
UNCOV
109
    if (!('event_name' in first) && ('event_group' in first)) {
×
110
      // Add a synthetic option value 'event_name' so the rest of the UI stays consistent
UNCOV
111
      withAlias.push('event_name');
×
112
    }
113

114
    // Numeric measures present in daily rollups
115
    const numericCandidates = [
×
116
      'events_count','users_count','sessions_count',
117
      'avg_duration_ms','p50_duration_ms','p90_duration_ms','p99_duration_ms',
118
      'http_error_rate','crash_rate_per_1k_sessions',
119
      'revenue_usd','purchase_count'
120
    ];
121
    const measures = numericCandidates.filter(k => k in first);
×
122

UNCOV
123
    this.dimensionOptions = [{ label: '-- Select Dimension --', value: '' },
×
124
      ...withAlias.map(k => ({ label: k === 'event_name' ? 'Event Type' : this.pretty(k), value: k }))
×
125
    ];
126

127
    this.measureOptions = measures.length
×
128
      ? measures.map(k => ({ label: this.pretty(k), value: k }))
×
129
      : [{ label: 'Events Count', value: 'events_count' }];
130
  }
131

132
  private pretty(key: string) {
UNCOV
133
    return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
×
134
  }
135

136
  get dimensionOptionsNoPlaceholder() {
UNCOV
137
    return this.dimensionOptions.filter(o => !!o.value);
×
138
  }
139

140
  async ngOnInit() {
141

UNCOV
142
    this.filters = [
×
143
      {name: 'Today\'s records', code: 'NY'},
144
      {name: 'This weeks\'s records', code: 'RM'},
145
      {name: 'This month\'s records', code: 'LDN'},
146
      {name: 'This year\'s records', code: 'IST'}
147
    ];
148

149
    // If data already present synchronously, ensure cards and views are populated
150
    if (this.data() && this.data().length > 0) {
×
151
      this.fetchCardStatistics();
×
152
      this.groupData();
×
UNCOV
153
      this.createPivotTable();
×
154
    }
155

156
  }
157

158
  onSubmit() {
159

UNCOV
160
    if (!this.dataGroupForm || this.dataGroupForm.invalid) {
×
UNCOV
161
      this.dataGroupForm?.markAllAsTouched();
×
162
      return;
×
163
    }
UNCOV
164
    this.groupData();
×
165
  }
166

167
  groupData() {
168

UNCOV
169
    this.groupTableLoaded = false;
×
UNCOV
170
      const { groupBy: dimension, aggregation, measure } = this.dataGroupForm.value;
×
171

172
    if (!dimension) {
×
UNCOV
173
      alert('Please select a dimension to group by');
×
174
      return;
×
175
    }
176

UNCOV
177
    const getDim = (it: any, key: string) => {
×
UNCOV
178
      if (key === 'event_name') {
×
179
        // Prefer event_name if present; otherwise fall back to event_group (Daily dataset)
180
        return (it?.event_name ?? it?.event_group ?? 'Unknown');
×
181
      }
UNCOV
182
      return (it?.[key] ?? 'Unknown');
×
183
    };
184

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

UNCOV
187
    this.data().forEach((item: any) => {
×
188
      const key = getDim(item, dimension);
×
UNCOV
189
      if (!groupedData[key]) {
×
190
        groupedData[key] = [];
×
191
      }
UNCOV
192
      groupedData[key].push(item);
×
193
    });
194

195
    // Apply aggregation
UNCOV
196
    const result: any[] = [];
×
UNCOV
197
    for (const key in groupedData) {
×
UNCOV
198
      const group = groupedData[key];
×
199
      let value: number = 0;
×
200

UNCOV
201
      switch (aggregation) {
×
202
        case 'count':
UNCOV
203
          value = group.length;
×
204
          break;
×
205
        case 'sum':
UNCOV
206
          value = group.reduce((sum: number, item: any) => sum + (Number(item?.[measure]) || 0), 0);
×
UNCOV
207
          break;
×
208
        case 'avg':
209
          const sum = group.reduce((total: number, item: any) => total + (Number(item?.[measure]) || 0), 0);
×
210
          value = group.length ? sum / group.length : 0;
×
211
          break;
×
212
        case 'min':
UNCOV
213
          value = Math.min(...group.map((item: any) => Number(item?.[measure]) || 0));
×
214
          break;
×
215
        case 'max':
216
          value = Math.max(...group.map((item: any) => Number(item?.[measure]) || 0));
×
UNCOV
217
          break;
×
218
      }
219

UNCOV
220
      result.push({
×
221
        [dimension]: key,
222
        value: Math.round(value * 100) / 100
223
      });
224
    }
225

226
    // Create grouped grid
227
    const columnDefs = [
×
228
      { headerName: dimension, field: dimension },
229
      { headerName: String(aggregation).toUpperCase(), field: 'value' }
230
    ];
231

232
    this.groupColumnDefs = [...columnDefs];
×
UNCOV
233
    this.groupRowData = [...result];
×
UNCOV
234
    this.groupTableLoaded = true;
×
235

236
  }
237

238
  onPivotSubmit() {
UNCOV
239
    if (!this.pivotTableForm || this.pivotTableForm.invalid) {
×
UNCOV
240
      this.pivotTableForm?.markAllAsTouched();
×
UNCOV
241
      return;
×
242
    }
243

244
    this.createPivotTable();
×
245

246

247

248

249

250
  }
251

252
  createPivotTable() {
253

UNCOV
254
    const { rowDimension, columnDimension, valueMeasure } = this.pivotTableForm.value;
×
255

UNCOV
256
    if (!rowDimension) {
×
UNCOV
257
      alert('Please select at least a row dimension');
×
UNCOV
258
      return;
×
259
    }
260

261
    // Group data by row and column dimensions
UNCOV
262
    const pivotData: any = {};
×
UNCOV
263
    const columnValues = new Set();
×
264

UNCOV
265
    const getDim = (it: any, key: string) => {
×
UNCOV
266
      if (!key) return 'Total';
×
UNCOV
267
      if (key === 'event_name') {
×
UNCOV
268
        return (it?.event_name ?? it?.event_group ?? 'Unknown');
×
269
      }
UNCOV
270
      return (it?.[key] ?? 'Unknown');
×
271
    };
272

UNCOV
273
    this.data().forEach((item: any) => {
×
UNCOV
274
      const rowValue = getDim(item, rowDimension);
×
UNCOV
275
      const colValue = columnDimension ? getDim(item, columnDimension) : 'Total';
×
276

277
      // Add to column values set
UNCOV
278
      if (columnDimension) columnValues.add(colValue);
×
279

UNCOV
280
      if (!pivotData[rowValue]) {
×
UNCOV
281
        pivotData[rowValue] = {};
×
282
      }
283

UNCOV
284
      if (!pivotData[rowValue][colValue]) {
×
UNCOV
285
        pivotData[rowValue][colValue] = { count: 0, sum: 0 };
×
286
      }
287

288
      // Update measures
UNCOV
289
      pivotData[rowValue][colValue].count += 1;
×
UNCOV
290
      if (valueMeasure !== 'count') {
×
UNCOV
291
        pivotData[rowValue][colValue].sum += Number(item?.[valueMeasure]) || 0;
×
292
      }
293
    });
294

295
    // Prepare column definitions
UNCOV
296
    const columnDefs: any = [
×
297
      { headerName: rowDimension, field: rowDimension, pinned: 'left' }
298
    ];
299

300
    // Add column dimension values as columns
UNCOV
301
    const columnArray: any = columnDimension ? Array.from(columnValues) : ['Total'];
×
UNCOV
302
    columnArray.forEach((col: string | number) => {
×
UNCOV
303
      columnDefs.push({
×
304
        headerName: col,
305
        valueGetter: (params: { data: { [x: string]: any; }; }) => {
UNCOV
306
          const rowValue = params.data[rowDimension];
×
UNCOV
307
          const cellData = pivotData[rowValue] && pivotData[rowValue][col];
×
UNCOV
308
          return cellData ? (valueMeasure === 'count' ? cellData.count : cellData.sum) : 0;
×
309
        }
310
      });
311
    });
312

313
    // Add total column only when there is a column dimension (to provide row totals)
UNCOV
314
    if (columnDimension) {
×
UNCOV
315
      columnDefs.push({
×
316
        headerName: 'Total',
317
        valueGetter: (params: { data: { [x: string]: any; }; }) => {
UNCOV
318
          const rowValue = params.data[rowDimension];
×
UNCOV
319
          let total = 0;
×
UNCOV
320
          for (const col in pivotData[rowValue]) {
×
UNCOV
321
            total += valueMeasure === 'count' ?
×
322
              pivotData[rowValue][col].count :
323
              pivotData[rowValue][col].sum;
324
          }
UNCOV
325
          return total;
×
326
        }
327
      });
328
    }
329

330
    // Prepare row data
UNCOV
331
    const rowData = Object.keys(pivotData).map(rowValue => {
×
UNCOV
332
      return {[rowDimension]: rowValue};
×
333
    });
334

335

UNCOV
336
    this.pivotColumnDefs = [...columnDefs];
×
UNCOV
337
    this.pivotRowData = [...rowData];
×
UNCOV
338
    this.pivotTableLoaded = true;
×
339

340

341
  }
342

343
  toggleFilterVisibility() {
UNCOV
344
    this.visible.update(v => !v);
×
345
  }
346

347
  private fetchCardStatistics() {
348

UNCOV
349
    const data = this.data();
×
350

UNCOV
351
    const totalEvents = data.reduce((acc: number, r: any) => acc + (Number(r?.events_count) || 0), 0);
×
UNCOV
352
    const totalUsers = data.reduce((acc: number, r: any) => acc + (Number(r?.users_count) || 0), 0);
×
353

UNCOV
354
    this.totalEvents = totalEvents;
×
UNCOV
355
    this.uniqueUsers = totalUsers;
×
356

UNCOV
357
    if (!data.length) {
×
UNCOV
358
      this.averageDuration = '0.00 ms';
×
UNCOV
359
      return;
×
360
    }
361

362
    // Average of avg_duration_ms across records that have it
UNCOV
363
    const durations = data
×
UNCOV
364
      .map((r: any) => Number(r?.avg_duration_ms))
×
UNCOV
365
      .filter((v: number) => !isNaN(v));
×
366

UNCOV
367
    if (durations.length === 0) {
×
UNCOV
368
      this.averageDuration = '0.00 ms';
×
UNCOV
369
      return;
×
370
    }
371

UNCOV
372
    const avg = durations.reduce((a: number, b: number) => a + b, 0) / durations.length;
×
UNCOV
373
    this.averageDuration = avg.toFixed(2) + ' ms';
×
374
  }
375

376
}
377

378
interface Filter {
379
  name: string,
380
  code: string
381
}
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