• 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

5.56
/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

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

15

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

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

41
  public data = this.dataService.data;
×
42
  loading = this.dataService.loading;
×
UNCOV
43
  error = this.dataService.error;
×
44

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

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

54

55
  filters!: Filter[];
56

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

61
  visible = signal(false);
×
62

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

66

67
  groupTableLoaded: boolean = true;
×
UNCOV
68
  pivotTableLoaded: boolean = true;
×
69

70
  // Card Statistics
UNCOV
71
  totalEvents = 0;
×
UNCOV
72
  averageDuration = '0ms';
×
UNCOV
73
  uniqueUsers = 0;
×
74
  // Card Statistics
75

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

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

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

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

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

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

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

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

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

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

137
  async ngOnInit() {
138

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

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

154
  onSubmit() {
155

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

163
  groupData() {
164

UNCOV
165
    this.groupTableLoaded = false;
×
UNCOV
166
      const { groupBy: dimension, aggregation, measure } = this.dataGroupForm.value;
×
167

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

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

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

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

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

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

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

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

224
  }
225

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

232
    this.createPivotTable();
×
233

234

235

236

237

238
  }
239

240
  createPivotTable() {
241

242
    const { rowDimension, columnDimension, valueMeasure } = this.pivotTableForm.value;
×
243

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

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

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

257
      // Add to column values set
UNCOV
258
      if (columnDimension) columnValues.add(colValue);
×
259

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

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

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

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

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

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

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

316

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

321

322
  }
323

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

328

329
  private fetchCardStatistics() {
330

UNCOV
331
    const data = this.data();
×
UNCOV
332
    this.totalEvents = data.length;
×
333

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

UNCOV
340
    let totalDuration = 0;
×
UNCOV
341
    const uniqueUserIds = new Set();
×
342

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

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

353
  }
354

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

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

364
interface Filter {
365
  name: string,
366
  code: string
367
}
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