• 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.24
/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 {Button, 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 { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';
1✔
12
import {PageHeaderComponent} from '../../shared/components/PageHeaderComponent/PageHeaderComponent';
1✔
13

1✔
14
ModuleRegistry.registerModules([AllCommunityModule]);
1✔
15

16

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

1✔
41
  private dataService = inject(DataService);
1✔
42

1✔
43
  public data = this.dataService.data;
44
  loading = this.dataService.loading;
45
  error = this.dataService.error;
1✔
46

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

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

1✔
56

1✔
57
  filters!: Filter[];
1✔
58

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

63
  visible = signal(false);
64

65
  dataGroupForm!: FormGroup;
66
  pivotTableForm!: FormGroup;
67

1✔
68

69
  groupTableLoaded: boolean = true;
70
  pivotTableLoaded: boolean = true;
71

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

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

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

91
    // React to data arrivals/changes to build options and initialize tables
92
    effect(() => {
93
      const records = this.data();
×
94
      this.buildSelectOptions(records);
95
      // Initialize when data available
96
      if (records && records.length > 0) {
97
        this.fetchCardStatistics();
×
98
        if (this.groupTableLoaded) {
99
          // no-op
100
        }
101
        this.groupData();
102
        this.createPivotTable();
1✔
UNCOV
103
      }
×
UNCOV
104
    });
×
105
  }
UNCOV
106

×
UNCOV
107
  private buildSelectOptions(records: any[]) {
×
UNCOV
108
    // Prefer known categorical dimensions; fall back to inferring string-like keys
×
109
    const defaultDims = ['platform','country','device_tier','event_name','release_channel','source','day','app_version','network_type'];
110
    const first = records?.[0] ?? {};
111
    const keys = Object.keys(first || {});
×
UNCOV
112

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

117
    // Numeric measures present in data
118
    const numericCandidates = ['count','duration_ms','revenue_usd','purchase_count'];
×
119
    const measures = numericCandidates.filter(k => k in first);
120

121
    this.dimensionOptions = [{ label: '-- Select Dimension --', value: '' },
122
      ...dims.map(k => ({ label: this.pretty(k), value: k }))
UNCOV
123
    ];
×
UNCOV
124

×
125
    this.measureOptions = [
×
UNCOV
126
      { label: 'Count', value: 'count' },
×
127
      ...measures.filter(m => m !== 'count').map(k => ({ label: this.pretty(k), value: k }))
128
    ];
129
  }
130

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

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

139
  async ngOnInit() {
140

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

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

UNCOV
156
  onSubmit() {
×
157

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

UNCOV
165
  groupData() {
×
166

167
    this.groupTableLoaded = false;
×
168
      const { groupBy: dimension, aggregation, measure } = this.dataGroupForm.value;
×
169

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

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

177
    this.data().forEach((item: any) => {
×
178
      const key = item?.[dimension] ?? 'Unknown';
×
179
      if (!groupedData[key]) {
180
        groupedData[key] = [];
×
UNCOV
181
      }
×
182
      groupedData[key].push(item);
183
    });
UNCOV
184

×
185
    // Apply aggregation
186
    const result: any[] = [];
187
    for (const key in groupedData) {
188
      const group = groupedData[key];
189
      let value: number = 0;
UNCOV
190

×
191
      switch (aggregation) {
192
        case 'count':
193
          value = group.length;
194
          break;
UNCOV
195
        case 'sum':
×
196
          value = group.reduce((sum: number, item: any) => sum + (Number(item?.[measure]) || 0), 0);
×
197
          break;
×
198
        case 'avg':
199
          const sum = group.reduce((total: number, item: any) => total + (Number(item?.[measure]) || 0), 0);
200
          value = group.length ? sum / group.length : 0;
201
          break;
UNCOV
202
        case 'min':
×
203
          value = Math.min(...group.map((item: any) => Number(item?.[measure]) || 0));
×
204
          break;
×
205
        case 'max':
206
          value = Math.max(...group.map((item: any) => Number(item?.[measure]) || 0));
207
          break;
×
208
      }
209

210
      result.push({
211
        [dimension]: key,
212
        value: Math.round(value * 100) / 100
213
      });
UNCOV
214
    }
×
215

UNCOV
216
    // Create grouped grid
×
217
    const columnDefs = [
×
UNCOV
218
      { headerName: dimension, field: dimension },
×
219
      { headerName: String(aggregation).toUpperCase(), field: 'value' }
220
    ];
221

222
    this.groupColumnDefs = [...columnDefs];
×
223
    this.groupRowData = [...result];
×
224
    this.groupTableLoaded = true;
UNCOV
225

×
UNCOV
226
  }
×
UNCOV
227

×
228
  onPivotSubmit() {
229
    if (!this.pivotTableForm || this.pivotTableForm.invalid) {
230
      this.pivotTableForm?.markAllAsTouched();
×
231
      return;
UNCOV
232
    }
×
UNCOV
233

×
234
    this.createPivotTable();
235

UNCOV
236

×
UNCOV
237

×
238

239

240
  }
UNCOV
241

×
242
  createPivotTable() {
UNCOV
243

×
244
    const { rowDimension, columnDimension, valueMeasure } = this.pivotTableForm.value;
×
245

246
    if (!rowDimension) {
247
      alert('Please select at least a row dimension');
248
      return;
UNCOV
249
    }
×
250

251
    // Group data by row and column dimensions
252
    const pivotData: any = {};
×
253
    const columnValues = new Set();
×
UNCOV
254

×
255
    this.data().forEach((item: any) => {
256
      const rowValue = item[rowDimension] || 'Unknown';
×
257
      const colValue = columnDimension ? (item[columnDimension] || 'Unknown') : 'Total';
×
UNCOV
258

×
259
      // Add to column values set
260
      if (columnDimension) columnValues.add(colValue);
261

262
      if (!pivotData[rowValue]) {
263
        pivotData[rowValue] = {};
UNCOV
264
      }
×
UNCOV
265

×
266
      if (!pivotData[rowValue][colValue]) {
267
        pivotData[rowValue][colValue] = { count: 0, sum: 0 };
×
UNCOV
268
      }
×
UNCOV
269

×
UNCOV
270
      // Update measures
×
271
      pivotData[rowValue][colValue].count += 1;
UNCOV
272
      // Only accumulate sum when valueMeasure is not 'count'
×
273
      if (valueMeasure !== 'count') {
274
        pivotData[rowValue][colValue].sum += Number(item?.[valueMeasure]) || 0;
275
      }
276
    });
277

UNCOV
278
    // Prepare column definitions
×
279
    const columnDefs: any = [
×
280
      { headerName: rowDimension, field: rowDimension, pinned: 'left' }
281
    ];
282

UNCOV
283
    // Add column dimension values as columns
×
284
    const columnArray: any = columnDimension ? Array.from(columnValues) : ['Total'];
×
285
    columnArray.forEach((col: string | number) => {
×
286
      columnDefs.push({
287
        headerName: col,
288
        valueGetter: (params: { data: { [x: string]: any; }; }) => {
289
          const rowValue = params.data[rowDimension];
290
          const cellData = pivotData[rowValue] && pivotData[rowValue][col];
291
          return cellData ? (valueMeasure === 'count' ? cellData.count : cellData.sum) : 0;
×
292
        }
293
      });
294
    });
×
295

UNCOV
296
    // Add total column only when there is a column dimension (to provide row totals)
×
297
    if (columnDimension) {
×
298
      columnDefs.push({
×
299
        headerName: 'Total',
UNCOV
300
        valueGetter: (params: { data: { [x: string]: any; }; }) => {
×
301
          const rowValue = params.data[rowDimension];
302
          let total = 0;
303
          for (const col in pivotData[rowValue]) {
304
            total += valueMeasure === 'count' ?
×
305
              pivotData[rowValue][col].count :
UNCOV
306
              pivotData[rowValue][col].sum;
×
307
          }
308
          return total;
×
309
        }
UNCOV
310
      });
×
311
    }
312

UNCOV
313
    // Prepare row data
×
314
    const rowData = Object.keys(pivotData).map(rowValue => {
315
      return {[rowDimension]: rowValue};
×
316
    });
UNCOV
317

×
UNCOV
318

×
319
    this.pivotColumnDefs = [...columnDefs];
×
320
    this.pivotRowData = [...rowData];
×
321
    this.pivotTableLoaded = true;
×
UNCOV
322

×
UNCOV
323

×
UNCOV
324
  }
×
325

326
  toggleFilterVisibility() {
327
    this.visible.update(v => !v);
UNCOV
328
  }
×
UNCOV
329

×
330

331
  private fetchCardStatistics() {
UNCOV
332

×
333
    const data = this.data();
×
334
    this.totalEvents = data.length;
×
UNCOV
335

×
336
    if (this.totalEvents === 0) {
×
337
      this.averageDuration = '0.00 ms';
×
338
      this.uniqueUsers = 0;
339
      return;
340
    }
341

×
342
    let totalDuration = 0;
343
    const uniqueUserIds = new Set();
×
UNCOV
344

×
345
    data.forEach((event: any) => {
×
346
      totalDuration += event.duration_ms || 0;
347
      if (event.user_pseudo_id) {
×
348
        uniqueUserIds.add(event.user_pseudo_id);
349
      }
350
    });
UNCOV
351

×
352
    this.averageDuration = (totalDuration / this.totalEvents).toFixed(2) + ' ms';
353
    this.uniqueUsers = uniqueUserIds.size;
×
UNCOV
354

×
355
  }
UNCOV
356

×
357
  exportPivotTable() {
358
    console.log('Exported Pivot Table')
UNCOV
359
  }
×
360

UNCOV
361
  exportGroupTable() {
×
362
    console.log('Exported Group Table')
UNCOV
363
  }
×
UNCOV
364
}
×
UNCOV
365

×
UNCOV
366
interface Filter {
×
UNCOV
367
  name: string,
×
UNCOV
368
  code: string
×
UNCOV
369
}
×
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