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

michaelmbugua-me / DataAnalyticsPlatform / #13

28 Aug 2025 09:30AM UTC coverage: 26.838% (+2.1%) from 24.769%
#13

push

github

michaelmbugua-me
feat: Overall Improvement in terms of responsiveness
- Clean up styling to ensure the portal is responsive on mobile devices.

85 of 743 branches covered (11.44%)

Branch coverage included in aggregate %.

9 of 12 new or added lines in 4 files covered. (75.0%)

566 existing lines in 10 files now uncovered.

488 of 1392 relevant lines covered (35.06%)

1.81 hits per line

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

12.98
/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
import {PageHeaderComponent} from '../../shared/components/PageHeaderComponent/PageHeaderComponent';
1✔
12
import {SplitButton} from 'primeng/splitbutton';
13

14

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

1✔
39
  private dataService = inject(DataService);
40

1✔
41
  public data = this.dataService.dailyRollups;
1✔
42
  loading = this.dataService.loading;
1✔
43
  error = this.dataService.error;
44

1✔
45
  groupColumnDefs: any[] = [];
1✔
46
  groupRowData: any[] = [];
1✔
47
  groupDefaultColDef = { sortable: true, filter: true, resizable: true };
1✔
48
  domLayout: 'autoHeight' = 'autoHeight';
49

1✔
50
  pivotColumnDefs: any[] = [];
1✔
51
  pivotRowData: any[] = [];
1✔
52
  pivotDefaultColDef = { sortable: true, filter: true, resizable: true };
53

54

55
  filters!: Filter[];
56

57
  // Dynamic select options derived from data
1✔
58
  dimensionOptions: { label: string; value: string }[] = [];
1✔
59
  measureOptions: { label: string; value: string }[] = [];
60

1✔
61
  visible = signal(false);
62

63
  dataGroupForm!: FormGroup;
64
  pivotTableForm!: FormGroup;
65

66

1✔
67
  groupTableLoaded: boolean = true;
1✔
68
  pivotTableLoaded: boolean = true;
69

70

71
  // Card Statistics
1✔
72
  totalEvents = 0;
1✔
73
  averageDuration = '0ms';
1✔
74
  uniqueUsers = 0;
75
  // Card Statistics
1✔
76

1✔
77
  constructor(private fb: FormBuilder) {
78
    this.dataGroupForm = fb.group({
79
      groupBy: ['country', Validators.required],
80
      aggregation: ['count', Validators.required],
81
      measure: ['events_count', Validators.required]
82
    });
1✔
83

84
    this.pivotTableForm = fb.group({
85
      rowDimension: ['platform', Validators.required],
86
      columnDimension: [''],
87
      valueMeasure: ['events_count', Validators.required]
88
    });
1✔
UNCOV
89

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

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

107
    const dims = defaultDims.filter(k => k in first).concat(
108
      keys.filter(k => typeof first[k] === 'string' && !defaultDims.includes(k))
×
UNCOV
109
    );
×
UNCOV
110

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

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

×
127
    this.dimensionOptions = [{ label: '-- Select Dimension --', value: '' },
×
128
      ...withAlias.map(k => ({ label: k === 'event_name' ? 'Event Type' : this.pretty(k), value: k }))
129
    ];
130

131
    this.measureOptions = measures.length
132
      ? measures.map(k => ({ label: this.pretty(k), value: k }))
×
133
      : [{ label: 'Events Count', value: 'events_count' }];
134
  }
135

UNCOV
136
  private pretty(key: string) {
×
137
    return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
138
  }
139

140
  get dimensionOptionsNoPlaceholder() {
141
    return this.dimensionOptions.filter(o => !!o.value);
×
142
  }
143

144
  async ngOnInit() {
145

146
    this.filters = [
147
      {name: 'Today\'s records', code: 'NY'},
148
      {name: 'This weeks\'s records', code: 'RM'},
UNCOV
149
      {name: 'This month\'s records', code: 'LDN'},
×
UNCOV
150
      {name: 'This year\'s records', code: 'IST'}
×
UNCOV
151
    ];
×
UNCOV
152

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

×
UNCOV
160
  }
×
UNCOV
161

×
162
  onSubmit() {
UNCOV
163

×
164
    if (!this.dataGroupForm || this.dataGroupForm.invalid) {
165
      this.dataGroupForm?.markAllAsTouched();
166
      return;
167
    }
168
    this.groupData();
×
UNCOV
169
  }
×
170

UNCOV
171
  groupData() {
×
UNCOV
172

×
173
    this.groupTableLoaded = false;
×
174
      const { groupBy: dimension, aggregation, measure } = this.dataGroupForm.value;
175

176
    if (!dimension) {
×
177
      alert('Please select a dimension to group by');
×
178
      return;
×
179
    }
UNCOV
180

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

×
189
    const groupedData: Record<string, any[]> = {};
UNCOV
190

×
191
    this.data().forEach((item: any) => {
192
      const key = getDim(item, dimension);
193
      if (!groupedData[key]) {
×
194
        groupedData[key] = [];
×
UNCOV
195
      }
×
196
      groupedData[key].push(item);
×
197
    });
UNCOV
198

×
199
    // Apply aggregation
200
    const result: any[] = [];
×
201
    for (const key in groupedData) {
×
202
      const group = groupedData[key];
203
      let value: number = 0;
×
UNCOV
204

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

224
      result.push({
×
225
        [dimension]: key,
226
        value: Math.round(value * 100) / 100
227
      });
228
    }
UNCOV
229

×
UNCOV
230
    // Create grouped grid
×
231
    const columnDefs = [
×
232
      { headerName: dimension, field: dimension },
233
      { headerName: String(aggregation).toUpperCase(), field: 'value' }
234
    ];
235

236
    this.groupColumnDefs = [...columnDefs];
×
237
    this.groupRowData = [...result];
×
238
    this.groupTableLoaded = true;
×
239

240
  }
UNCOV
241

×
242
  onPivotSubmit() {
243
    if (!this.pivotTableForm || this.pivotTableForm.invalid) {
244
      this.pivotTableForm?.markAllAsTouched();
245
      return;
246
    }
UNCOV
247

×
248
    this.createPivotTable();
UNCOV
249

×
UNCOV
250

×
UNCOV
251

×
252

253

UNCOV
254
  }
×
UNCOV
255

×
256
  createPivotTable() {
UNCOV
257

×
258
    const { rowDimension, columnDimension, valueMeasure } = this.pivotTableForm.value;
×
UNCOV
259

×
260
    if (!rowDimension) {
×
261
      alert('Please select at least a row dimension');
262
      return;
×
263
    }
264

UNCOV
265
    // Group data by row and column dimensions
×
266
    const pivotData: any = {};
×
267
    const columnValues = new Set();
×
268

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

×
277
    this.data().forEach((item: any) => {
278
      const rowValue = getDim(item, rowDimension);
279
      const colValue = columnDimension ? getDim(item, columnDimension) : 'Total';
×
UNCOV
280

×
UNCOV
281
      // Add to column values set
×
282
      if (columnDimension) columnValues.add(colValue);
283

284
      if (!pivotData[rowValue]) {
285
        pivotData[rowValue] = {};
×
286
      }
287

288
      if (!pivotData[rowValue][colValue]) {
289
        pivotData[rowValue][colValue] = { count: 0, sum: 0 };
×
UNCOV
290
      }
×
UNCOV
291

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

299
    // Prepare column definitions
300
    const columnDefs: any = [
UNCOV
301
      { headerName: rowDimension, field: rowDimension, pinned: 'left' }
×
UNCOV
302
    ];
×
303

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

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

334
    // Prepare row data
335
    const rowData = Object.keys(pivotData).map(rowValue => {
×
336
      return {[rowDimension]: rowValue};
UNCOV
337
    });
×
UNCOV
338

×
339

340
    this.pivotColumnDefs = [...columnDefs];
×
341
    this.pivotRowData = [...rowData];
×
342
    this.pivotTableLoaded = true;
UNCOV
343

×
UNCOV
344

×
UNCOV
345
  }
×
346

347
  toggleFilterVisibility() {
348
    this.visible.update(v => !v);
×
UNCOV
349
  }
×
UNCOV
350

×
351
  private fetchCardStatistics() {
UNCOV
352

×
353
    const data = this.data();
×
UNCOV
354

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

×
358
    this.totalEvents = totalEvents;
×
359
    this.uniqueUsers = totalUsers;
360

361
    if (!data.length) {
362
      this.averageDuration = '0.00 ms';
363
      return;
364
    }
365

366
    // Average of avg_duration_ms across records that have it
367
    const durations = data
368
      .map((r: any) => Number(r?.avg_duration_ms))
369
      .filter((v: number) => !isNaN(v));
370

371
    if (durations.length === 0) {
372
      this.averageDuration = '0.00 ms';
373
      return;
374
    }
375

376
    const avg = durations.reduce((a: number, b: number) => a + b, 0) / durations.length;
377
    this.averageDuration = avg.toFixed(2) + ' ms';
378
  }
379

380
}
381

382
interface Filter {
383
  name: string,
384
  code: string
385
}
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