From 91b835f47aaf0a714c4d57b419e0c53a40d59b99 Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Tue, 19 Nov 2024 19:01:37 -0500 Subject: [PATCH] Refactor Highcharts integration and improve chart options handling; update component templates for better structure --- frontend/src/app/highcharts.theme.ts | 33 +- ...oard-card-drilldown-bar-chart.component.ts | 5 +- .../copilot-metrics-pie-chart.component.ts | 224 +----------- .../copilot-metrics.component.html | 17 +- .../copilot-metrics.component.ts | 22 +- .../copilot-seat/copilot-seat.component.ts | 5 +- .../copilot-surveys.component.ts | 2 +- .../adoption-chart.component.html | 2 +- .../adoption-chart.component.ts | 102 +++--- .../daily-activity-chart.component.html | 2 +- .../daily-activity-chart.component.ts | 60 ++-- .../time-saved-chart.component.html | 2 +- .../time-saved-chart.component.ts | 139 ++------ .../copilot-value/value.component.html | 6 +- .../copilot/copilot-value/value.component.ts | 50 ++- .../src/app/services/highcharts.service.ts | 319 +++++++++++++----- .../services/metrics.service.interfaces.ts | 66 ++-- .../date-range-select.component.ts | 7 +- 18 files changed, 468 insertions(+), 595 deletions(-) diff --git a/frontend/src/app/highcharts.theme.ts b/frontend/src/app/highcharts.theme.ts index 1aa976e..04cf834 100644 --- a/frontend/src/app/highcharts.theme.ts +++ b/frontend/src/app/highcharts.theme.ts @@ -1,4 +1,4 @@ -import Highcharts from 'highcharts/es-modules/masters/highcharts.src'; +import * as Highcharts from 'highcharts'; const tooltipHeaderFormat = ''; @@ -36,7 +36,7 @@ const yAxisConfig: Highcharts.YAxisOptions = { ...xAxisConfig }; -Highcharts.theme = { +const theme: Highcharts.Options = { colors: [ 'var(--sys-primary)', 'var(--sys-secondary)', @@ -49,7 +49,7 @@ Highcharts.theme = { 'var(--sys-on-error)' ], chart: { - backgroundColor: 'var(--sys-surface)', + backgroundColor: undefined, // 'var(--sys-surface)', borderRadius: 16, style: { fontFamily: 'var(--sys-body-large-font)' @@ -131,8 +131,7 @@ Highcharts.theme = { color: 'var(--sys-on-surface)' } } - }, - borderRadius: 4 + } }, separator: { style: { @@ -255,16 +254,18 @@ Highcharts.theme = { pie: { borderWidth: 0, borderRadius: 4, - dataLabels: { style: { - font: 'var(--sys-label-large)', - fontSize: '14px', - opacity: 0.87, - fontWeight: 'var(--sys-label-large-weight)', - textOutline: 'none', - }, - distance: 20, - connectorWidth: 1, - connectorColor: 'var(--sys-outline-variant)' + dataLabels: { + style: { + font: 'var(--sys-label-large)', + color: 'var(--sys-on-surface)', + fontSize: '14px', + opacity: 0.87, + fontWeight: 'var(--sys-label-large-weight)', + textOutline: 'none', + }, + distance: 20, + connectorWidth: 1, + connectorColor: 'var(--sys-outline-variant)' } } }, @@ -286,4 +287,4 @@ Highcharts.theme = { enabled: false } }; -Highcharts.setOptions(Highcharts.theme); +Highcharts.setOptions(theme); diff --git a/frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component.ts b/frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component.ts index e105e41..5886582 100644 --- a/frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-dashboard/dashboard-card/dashboard-card-drilldown-bar-chart/dashboard-card-drilldown-bar-chart.component.ts @@ -1,7 +1,8 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; -import Highcharts from 'highcharts/es-modules/masters/highcharts.src'; -import 'highcharts/es-modules/masters/modules/drilldown.src'; +import * as Highcharts from 'highcharts'; +import HC_drilldown from 'highcharts/modules/drilldown'; +HC_drilldown(Highcharts); import { HighchartsChartModule } from 'highcharts-angular'; import { CommonModule } from '@angular/common'; import { HighchartsService } from '../../../../../services/highcharts.service'; diff --git a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics-pie-chart/copilot-metrics-pie-chart.component.ts b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics-pie-chart/copilot-metrics-pie-chart.component.ts index bb08748..87384a7 100644 --- a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics-pie-chart/copilot-metrics-pie-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics-pie-chart/copilot-metrics-pie-chart.component.ts @@ -1,6 +1,6 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; import { CopilotMetrics } from '../../../../services/metrics.service.interfaces'; -import Highcharts from 'highcharts/es-modules/masters/highcharts.src'; +import * as Highcharts from 'highcharts'; import { HighchartsChartModule } from 'highcharts-angular'; import { HighchartsService } from '../../../../services/highcharts.service'; @@ -15,13 +15,10 @@ import { HighchartsService } from '../../../../services/highcharts.service'; // styleUrl: './copilot-metrics-ide-completion-pie-chart.component.css', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CopilotMetricsPieChartComponent { +export class CopilotMetricsPieChartComponent implements OnChanges { Highcharts: typeof Highcharts = Highcharts; @Input() metricsTotals?: CopilotMetrics; chartOptions: Highcharts.Options = { - chart: { - type: 'pie' - }, tooltip: { headerFormat: '{series.name}
', pointFormat: '{point.name}: ' + @@ -33,212 +30,7 @@ export class CopilotMetricsPieChartComponent { type: 'pie', }], drilldown: { - series: [ - { - type: 'pie', - name: 'Chrome', - id: 'Chrome', - data: [ - [ - 'v97.0', - 36.89 - ], - [ - 'v96.0', - 18.16 - ], - [ - 'v95.0', - 0.54 - ], - [ - 'v94.0', - 0.7 - ], - [ - 'v93.0', - 0.8 - ], - [ - 'v92.0', - 0.41 - ], - [ - 'v91.0', - 0.31 - ], - [ - 'v90.0', - 0.13 - ], - [ - 'v89.0', - 0.14 - ], - [ - 'v88.0', - 0.1 - ], - [ - 'v87.0', - 0.35 - ], - [ - 'v86.0', - 0.17 - ], - [ - 'v85.0', - 0.18 - ], - [ - 'v84.0', - 0.17 - ], - [ - 'v83.0', - 0.21 - ], - [ - 'v81.0', - 0.1 - ], - [ - 'v80.0', - 0.16 - ], - [ - 'v79.0', - 0.43 - ], - [ - 'v78.0', - 0.11 - ], - [ - 'v76.0', - 0.16 - ], - [ - 'v75.0', - 0.15 - ], - [ - 'v72.0', - 0.14 - ], - [ - 'v70.0', - 0.11 - ], - [ - 'v69.0', - 0.13 - ], - [ - 'v56.0', - 0.12 - ], - [ - 'v49.0', - 0.17 - ] - ] - }, - { - type: 'pie', - name: 'Safari', - id: 'Safari', - data: [ - [ - 'v15.3', - 0.1 - ], - [ - 'v15.2', - 2.01 - ], - [ - 'v15.1', - 2.29 - ], - [ - 'v15.0', - 0.49 - ], - [ - 'v14.1', - 2.48 - ], - [ - 'v14.0', - 0.64 - ], - [ - 'v13.1', - 1.17 - ], - [ - 'v13.0', - 0.13 - ], - [ - 'v12.1', - 0.16 - ] - ] - }, - { - type: 'pie', - name: 'Edge', - id: 'Edge', - data: [ - [ - 'v97', - 6.62 - ], - [ - 'v96', - 2.55 - ], - [ - 'v95', - 0.15 - ] - ] - }, - { - type: 'pie', - name: 'Firefox', - id: 'Firefox', - data: [ - [ - 'v96.0', - 4.17 - ], - [ - 'v95.0', - 3.33 - ], - [ - 'v94.0', - 0.11 - ], - [ - 'v91.0', - 0.23 - ], - [ - 'v78.0', - 0.16 - ], - [ - 'v52.0', - 0.15 - ] - ] - } - ] + series: [] } }; _chartOptions?: Highcharts.Options; @@ -247,18 +39,14 @@ export class CopilotMetricsPieChartComponent { constructor( private highchartsService: HighchartsService, private cdr: ChangeDetectorRef - ) { - console.log(this.metricsTotals); - } + ) { } ngOnChanges() { if (this.metricsTotals) { - this._chartOptions = this.highchartsService.transformMetricsToPieDrilldown(this.metricsTotals); this.chartOptions = { ...this.chartOptions, - ...this._chartOptions + ...this.highchartsService.transformMetricsToPieDrilldown(this.metricsTotals) }; - console.log('now', this.chartOptions); this.updateFlag = true; this.cdr.detectChanges(); } diff --git a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html index b5f74c4..09b111b 100644 --- a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html +++ b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.html @@ -2,11 +2,20 @@ - - - +

+ +

- +
+ + + IDE Completions + + + + + +
\ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts index 4563985..c8e9a34 100644 --- a/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts +++ b/frontend/src/app/main/copilot/copilot-metrics/copilot-metrics.component.ts @@ -3,16 +3,21 @@ import { DateRangeSelectComponent } from "../../../shared/date-range-select/date import { MetricsService } from '../../../services/metrics.service'; import { CopilotMetrics } from '../../../services/metrics.service.interfaces'; import { CopilotMetricsPieChartComponent } from './copilot-metrics-pie-chart/copilot-metrics-pie-chart.component'; +import { MatCardModule } from '@angular/material/card'; @Component({ selector: 'app-metrics', standalone: true, imports: [ DateRangeSelectComponent, - CopilotMetricsPieChartComponent + CopilotMetricsPieChartComponent, + MatCardModule ], templateUrl: './copilot-metrics.component.html', - styleUrl: './copilot-metrics.component.scss' + styleUrls: [ + './copilot-metrics.component.scss', + '../copilot-dashboard/dashboard.component.scss' + ] }) export class CopilotMetricsComponent { metrics?: CopilotMetrics[]; @@ -23,17 +28,20 @@ export class CopilotMetricsComponent { ) { } dateRangeChange(event: {start: Date, end: Date}) { - console.log(event) + const utcStart = Date.UTC(event.start.getFullYear(), event.start.getMonth(), event.start.getDate()); + const utcEnd = Date.UTC(event.end.getFullYear(), event.end.getMonth(), event.end.getDate()); + const startModified = new Date(utcStart - 1); + const endModified = new Date(utcEnd + 1); this.metricsService.getMetrics({ - since: event.start.toISOString(), - until: event.end.toISOString() + since: startModified.toISOString(), + until: endModified.toISOString() }).subscribe((metrics) => { this.metrics = metrics; console.log(metrics); }); this.metricsService.getMetricsTotals({ - since: event.start.toISOString(), - until: event.end.toISOString() + since: startModified.toISOString(), + until: endModified.toISOString() }).subscribe((metricsTotals) => { this.metricsTotals = metricsTotals; console.log(metricsTotals); diff --git a/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.ts b/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.ts index 2eb5b96..e9d35ef 100644 --- a/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.ts +++ b/frontend/src/app/main/copilot/copilot-seats/copilot-seat/copilot-seat.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import Highcharts from 'highcharts/es-modules/masters/highcharts.src'; -import 'highcharts/es-modules/masters/modules/gantt.src'; +import * as Highcharts from 'highcharts'; +import HC_gantt from 'highcharts/modules/gantt'; +HC_gantt(Highcharts); import { HighchartsChartModule } from 'highcharts-angular'; import { Seat, SeatService } from '../../../../services/seat.service'; import { ActivatedRoute } from '@angular/router'; diff --git a/frontend/src/app/main/copilot/copilot-surveys/copilot-surveys.component.ts b/frontend/src/app/main/copilot/copilot-surveys/copilot-surveys.component.ts index eaa68e0..676365a 100644 --- a/frontend/src/app/main/copilot/copilot-surveys/copilot-surveys.component.ts +++ b/frontend/src/app/main/copilot/copilot-surveys/copilot-surveys.component.ts @@ -58,7 +58,7 @@ export class CopilotSurveysComponent implements OnInit { } } - onSurveyClick(survey: any) { + onSurveyClick(survey: Survey) { this.router.navigate(['/copilot/surveys', survey.id]); } } diff --git a/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.html b/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.html index 6f70c35..c9e1823 100644 --- a/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.html +++ b/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.html @@ -1,2 +1,2 @@ - + \ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.ts b/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.ts index 1dbb1f4..87d3cc8 100644 --- a/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/adoption-chart/adoption-chart.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import Highcharts from 'highcharts/es-modules/masters/highcharts.src'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import * as Highcharts from 'highcharts'; import { HighchartsChartModule } from 'highcharts-angular'; import { ActivityResponse } from '../../../../services/seat.service'; import { HighchartsService } from '../../../../services/highcharts.service'; @@ -17,12 +17,14 @@ import { DatePipe } from '@angular/common'; templateUrl: './adoption-chart.component.html', styleUrl: './adoption-chart.component.scss' }) -export class AdoptionChartComponent implements OnChanges { +export class AdoptionChartComponent implements OnInit, OnChanges { Highcharts: typeof Highcharts = Highcharts; updateFlag = false; totalUsers = 500; @Input() data?: ActivityResponse; - chartOptions: Highcharts.Options = { + @Input() stripped = false; + @Input() chartOptions?: Highcharts.Options; + notStrippedChartOptions: Highcharts.Options = { chart: { zooming: { type: 'x' @@ -71,84 +73,60 @@ export class AdoptionChartComponent implements OnChanges { } } }] - }, - tooltip: { - // '{point.x:%b %d, %Y}
', - headerFormat: '', - pointFormatter: function () { - const parts = [ - `${new DatePipe('en-US').transform(this.x)}
`, - `${this.series.name}: `, - `${(this as any).raw}`, - `(${this.y?.toFixed(1)}%)` - ] - return parts.join(''); - }, - style: { - fontSize: '14px' + } + } + _chartOptions: Highcharts.Options = { + yAxis: { + min: 0, + max: 100, + labels: { + enabled: false } }, series: [{ name: 'Users', type: 'spline', - data: [], - lineWidth: 2, - marker: { - enabled: true, - radius: 4, - symbol: 'circle' - }, - states: { - hover: { - lineWidth: 3 - } - } + data: [] }], + tooltip: { + headerFormat: '', + }, legend: { enabled: false }, - plotOptions: { - series: { - animation: { - duration: 300 - } - } - } }; - _chartOptions?: Highcharts.Options; + @Output() chartInstanceChange = new EventEmitter(); + strippedChartOptions: Highcharts.Options = JSON.parse(JSON.stringify(this._chartOptions)); + charts: Highcharts.Chart[] = []; constructor( private highchartsService: HighchartsService, - ) { + ) { } + + ngOnInit() { + this._chartOptions = { + ...this._chartOptions, + ...this.chartOptions + } + this._chartOptions = this.stripped ? this.strippedChartOptions : { + ...this._chartOptions, + ...this.notStrippedChartOptions + }; } ngOnChanges(changes: SimpleChanges) { if (changes['data'] && this.data) { - this._chartOptions = this.highchartsService.transformActivityMetricsToLine(this.data); - this.setTooltipFormatter(); - this.chartOptions = { - ...this.chartOptions, - ...this._chartOptions + const options = this.highchartsService.transformActivityMetricsToLine(this.data); + this._chartOptions = { + ...this._chartOptions, + ...options, + tooltip: { + ...options.tooltip, + ...this._chartOptions.tooltip + } }; this.updateFlag = true; } } - setTooltipFormatter() { - if (!this.data) return; - const dateTimes = Object.keys(this.data); - const isDaily = Math.abs(new Date(dateTimes[1]).getTime() - new Date(dateTimes[0]).getTime()) > 3600000; - const dateFormat = isDaily ? undefined : 'short'; - console.log(isDaily, dateFormat); - this.chartOptions.tooltip!.pointFormatter = function () { - const parts = [ - `${new DatePipe('en-US').transform(this.x, dateFormat)}
`, - `${this.series.name}: `, - `${(this as any).raw}`, - `(${this.y?.toFixed(1)}%)` - ] - return parts.join(''); - }; - } - } diff --git a/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.html b/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.html index c3c2617..6682a80 100644 --- a/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.html +++ b/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.html @@ -1,2 +1,2 @@ - + diff --git a/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.ts b/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.ts index 8dddf9f..7e5c5a8 100644 --- a/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/daily-activity-chart/daily-activity-chart.component.ts @@ -1,5 +1,5 @@ -import { Component, Input, OnChanges } from '@angular/core'; -import Highcharts from 'highcharts/es-modules/masters/highcharts.src'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import * as Highcharts from 'highcharts'; import { HighchartsChartModule } from 'highcharts-angular'; import { ActivityResponse } from '../../../../services/seat.service'; import { CopilotMetrics } from '../../../../services/metrics.service.interfaces'; @@ -14,38 +14,19 @@ import { HighchartsService } from '../../../../services/highcharts.service'; templateUrl: './daily-activity-chart.component.html', styleUrl: './daily-activity-chart.component.scss' }) -export class DailyActivityChartComponent implements OnChanges { +export class DailyActivityChartComponent implements OnInit, OnChanges { Highcharts: typeof Highcharts = Highcharts; updateFlag = false; - totalUsers = 500; @Input() activity?: ActivityResponse; @Input() metrics?: CopilotMetrics[]; - - chartOptions: Highcharts.Options = { - chart: { - zooming: { - type: 'x' - }, - width: undefined, - }, - xAxis: { - type: 'datetime', - dateTimeLabelFormats: { - // don't display the year - month: '%b', - year: '%b' - }, - crosshair: true - }, + @Input() chartOptions?: Highcharts.Options; + @Output() chartInstanceChange = new EventEmitter(); + _chartOptions: Highcharts.Options = { yAxis: { title: { text: 'Average Activity Per User' }, min: 0, - // max: 100, - // labels: { - // format: '{value}%' - // }, plotBands: [{ from: 500, to: 750, @@ -97,30 +78,27 @@ export class DailyActivityChartComponent implements OnChanges { name: '.COM Pull Requests', type: 'spline', }], - // legend: { - // enabled: false - // }, - plotOptions: { - series: { - animation: { - duration: 300 - } - } + legend: { + enabled: true } }; - _chartOptions?: Highcharts.Options; constructor( private highchartsService: HighchartsService - ) { - } + ) { } + ngOnInit() { + this._chartOptions = { + ...this._chartOptions, + ...this.chartOptions + } + } + ngOnChanges() { if (this.activity && this.metrics) { - this._chartOptions = this.highchartsService.transformMetricsToDailyActivityLine(this.activity, this.metrics); - this.chartOptions = { - ...this.chartOptions, - ...this._chartOptions + this._chartOptions = { + ...this._chartOptions, + ...this.highchartsService.transformMetricsToDailyActivityLine(this.activity, this.metrics) }; this.updateFlag = true; } diff --git a/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.html b/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.html index c3c2617..6682a80 100644 --- a/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.html +++ b/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.html @@ -1,2 +1,2 @@ - + diff --git a/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts b/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts index 705bd0b..dc94518 100644 --- a/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/time-saved-chart/time-saved-chart.component.ts @@ -1,5 +1,5 @@ -import { Component, Input } from '@angular/core'; -import Highcharts from 'highcharts/es-modules/masters/highcharts.src'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import * as Highcharts from 'highcharts'; import { HighchartsChartModule } from 'highcharts-angular'; import { Survey } from '../../../../services/copilot-survey.service'; import { HighchartsService } from '../../../../services/highcharts.service'; @@ -13,99 +13,19 @@ import { HighchartsService } from '../../../../services/highcharts.service'; templateUrl: './time-saved-chart.component.html', styleUrl: './time-saved-chart.component.scss' }) -export class TimeSavedChartComponent { +export class TimeSavedChartComponent implements OnInit, OnChanges { @Input() surveys?: Survey[]; + @Input() chartOptions?: Highcharts.Options; + @Output() chartInstanceChange = new EventEmitter(); Highcharts: typeof Highcharts = Highcharts; updateFlag = false; - data = [ - // April 2023 - [1680307200000, 6.2], // Week 1 - [1680912000000, 7.1], // Week 2 - [1681516800000, 6.8], // Week 3 - [1682121600000, 19.2], // Week 4 - 🚀 Anomaly spike - // May 2023 - [1682726400000, 7.8], // Week 5 - [1683331200000, 8.2], // Week 6 - [1683936000000, 5.1], // Week 7 - 📉 Anomaly drop - [1684540800000, 8.9], // Week 8 - // June 2023 - [1685145600000, 9.3], // Week 9 - [1685750400000, 8.7], // Week 10 - [1686355200000, 9.8], // Week 11 - [1686960000000, 9.2], // Week 12 - // July 2023 - [1687564800000, 18.8], // Week 13 - 🚀 Anomaly spike - [1688169600000, 10.1], // Week 14 - [1688774400000, 10.8], // Week 15 - [1689379200000, 10.2], // Week 16 - // August 2023 - [1689984000000, 11.4], // Week 17 - [1690588800000, 10.9], // Week 18 - [1691193600000, 5.3], // Week 19 - 📉 Anomaly drop - [1691798400000, 11.8], // Week 20 - // September 2023 - [1692403200000, 11.2], // Week 21 - [1693008000000, 12.5], // Week 22 - [1693612800000, 12.0], // Week 23 - [1694217600000, 19.5], // Week 24 - 🚀 Anomaly spike - // October 2023 - [1694822400000, 12.8], // Week 25 - [1695427200000, 12.3], // Week 26 - [1696032000000, 13.4], // Week 27 - [1696636800000, 12.9], // Week 28 - // November 2023 - [1697241600000, 13.8], // Week 29 - [1697846400000, 13.2], // Week 30 - [1698451200000, 14.3], // Week 31 - [1699056000000, 13.9], // Week 32 - // December 2023 - [1699660800000, 19.8], // Week 33 - 🚀 Anomaly spike - [1700265600000, 14.5], // Week 34 - [1700870400000, 15.1], // Week 35 - [1701475200000, 14.8], // Week 36 - // January 2024 - [1702080000000, 15.6], // Week 37 - [1702684800000, 15.2], // Week 38 - [1703289600000, 5.5], // Week 39 - 📉 Anomaly drop - [1703894400000, 15.9], // Week 40 - // February 2024 - [1704499200000, 16.4], // Week 41 - [1705104000000, 15.9], // Week 42 - [1705708800000, 16.8], // Week 43 - [1706313600000, 16.3], // Week 44 - // March 2024 - [1706918400000, 19.9], // Week 45 - 🚀 Final spike - [1707523200000, 17.2], // Week 46 - [1708128000000, 17.8], // Week 47 - [1708732800000, 17.4], // Week 48 - [1709337600000, 18.2], // Week 49 - [1709942400000, 17.9], // Week 50 - [1710547200000, 18.5], // Week 51 - [1711152000000, 18.8] // Week 52 - Final - ]; - - chartOptions: Highcharts.Options = { - chart: { - zooming: { - type: 'x' - }, - width: undefined, - }, - xAxis: { - type: 'datetime', - dateTimeLabelFormats: { - // don't display the year - month: '%b', - year: '%b' - }, - crosshair: true - }, + _chartOptions: Highcharts.Options = { yAxis: { title: { text: 'Time Saved (%)' }, min: 0, - // max: 100, + max: 100, labels: { format: '{value}%' }, @@ -148,54 +68,33 @@ export class TimeSavedChartComponent { series: [{ name: 'Time Saved', type: 'spline', - data: this.data.map(point => ({ - x: point[0], - y: point[1], - })), - lineWidth: 2, - marker: { - enabled: true, - radius: 4, - symbol: 'circle' - }, - states: { - hover: { - lineWidth: 3 - } - } + data: [] }, { type: 'scatter', name: 'Observations', - data: [], - marker: { - radius: 4 - } + data: [] }], legend: { enabled: false - }, - plotOptions: { - series: { - animation: { - duration: 300 - } - } } }; - _chartOptions?: Highcharts.Options; constructor( private highchartsService: HighchartsService - ) { + ) { } + + ngOnInit() { + this._chartOptions = { + ...this._chartOptions, + ...this.chartOptions + } } ngOnChanges() { if (this.surveys) { - console.log('surveys', this.surveys); - this._chartOptions = this.highchartsService.transformSurveysToScatter(this.surveys); - this.chartOptions = { - ...this.chartOptions, - ...this._chartOptions + this._chartOptions = { + ...this._chartOptions, + ...this.highchartsService.transformSurveysToScatter(this.surveys) }; this.updateFlag = true; } diff --git a/frontend/src/app/main/copilot/copilot-value/value.component.html b/frontend/src/app/main/copilot/copilot-value/value.component.html index bdefac2..ba2ede4 100644 --- a/frontend/src/app/main/copilot/copilot-value/value.component.html +++ b/frontend/src/app/main/copilot/copilot-value/value.component.html @@ -30,19 +30,19 @@

Value

Adoption - + Daily Activity (Usage) - + Time Saved - + \ No newline at end of file diff --git a/frontend/src/app/main/copilot/copilot-value/value.component.ts b/frontend/src/app/main/copilot/copilot-value/value.component.ts index 3eb9953..65e26a2 100644 --- a/frontend/src/app/main/copilot/copilot-value/value.component.ts +++ b/frontend/src/app/main/copilot/copilot-value/value.component.ts @@ -9,6 +9,7 @@ import { MetricsService } from '../../../services/metrics.service'; import { FormControl } from '@angular/forms'; import { combineLatest, startWith } from 'rxjs'; import { CopilotSurveyService, Survey } from '../../../services/copilot-survey.service'; +import * as Highcharts from 'highcharts'; @Component({ selector: 'app-value', @@ -20,7 +21,10 @@ import { CopilotSurveyService, Survey } from '../../../services/copilot-survey.s TimeSavedChartComponent ], templateUrl: './value.component.html', - styleUrl: './value.component.scss' + styleUrls: [ + './value.component.scss', + // '../copilot-dashboard/dashboard.component.scss' + ] }) export class CopilotValueComponent implements OnInit { activityData?: ActivityResponse; @@ -28,6 +32,32 @@ export class CopilotValueComponent implements OnInit { surveysData?: Survey[]; daysInactive = new FormControl(30); adoptionFidelity = new FormControl<'day' | 'hour'>('day'); + Highcharts: typeof Highcharts = Highcharts; + charts = [] as Highcharts.Chart[]; + chartOptions: Highcharts.Options = { + chart: { + zooming: { + type: 'x' + }, + width: undefined, + }, + xAxis: { + type: 'datetime', + dateTimeLabelFormats: { + // don't display the year + month: '%b', + year: '%b' + }, + crosshair: true + }, + plotOptions: { + series: { + animation: { + duration: 300 + } + } + }, + }; constructor( private seatService: SeatService, @@ -51,4 +81,22 @@ export class CopilotValueComponent implements OnInit { this.surveysData = data; }); } + + chartChanged(chart: Highcharts.Chart) { + this.charts.push(chart); + const charts = this.charts; + for (chart of charts) { + chart.xAxis[0].update({ + events: { + afterSetExtremes: function (event) { + charts.forEach(otherChart => { + if (otherChart.xAxis[0].min != event.min || otherChart.xAxis[0].max != event.max) { + otherChart.xAxis[0].setExtremes(event.min, event.max) + } + }) + } + } + }) + } + } } diff --git a/frontend/src/app/services/highcharts.service.ts b/frontend/src/app/services/highcharts.service.ts index 738aa52..0a6689f 100644 --- a/frontend/src/app/services/highcharts.service.ts +++ b/frontend/src/app/services/highcharts.service.ts @@ -1,28 +1,26 @@ import { Injectable } from '@angular/core'; -import { CopilotMetrics } from './metrics.service.interfaces'; +import { ChatModel, CodeModel, CopilotMetrics, DotComChatShared, DotComPullRequestsShared, IdeChatShared, IdeCodeCompletionsShared } from './metrics.service.interfaces'; import { DashboardCardBarsInput } from '../main/copilot/copilot-dashboard/dashboard-card/dashboard-card-bars/dashboard-card-bars.component'; import { ActivityResponse, Seat } from './seat.service'; -import Highcharts from 'highcharts/es-modules/masters/highcharts.src'; +import * as Highcharts from 'highcharts'; import { Survey } from './copilot-survey.service'; -import { DecimalPipe } from '@angular/common'; +import { DatePipe, DecimalPipe } from '@angular/common'; -interface CustomHighchartsPoint extends Highcharts.PointOptionsObject { +interface CustomHighchartsPoint extends Highcharts.Point { date?: Date; // eslint-disable-next-line @typescript-eslint/no-explicit-any raw?: any; + customTooltipInfo?: () => string; } interface CustomHighchartsGanttPoint extends Highcharts.GanttPointOptionsObject { + // eslint-disable-next-line @typescript-eslint/no-explicit-any raw?: any; } @Injectable({ providedIn: 'root' }) export class HighchartsService { - - constructor( - ) { } - transformCopilotMetricsToBarChartDrilldown(data: CopilotMetrics[]): Highcharts.Options { const engagedUsersSeries: Highcharts.SeriesOptionsType = { name: 'Users', @@ -43,6 +41,15 @@ export class HighchartsService { data.forEach(dateData => { const dateSeriesId = `date_${dateData.date}`; + const customTooltipInfo = { + 'copilot_ide_code_completions': (data: IdeCodeCompletionsShared) => `Suggestions: ${data.total_code_suggestions} +
Accepted: ${data.total_code_acceptances} (${((data.total_code_acceptances / data.total_code_suggestions) * 100).toFixed(2)}%) +
LoC suggested: ${data.total_code_lines_suggested} +
LoC accepted: ${data.total_code_lines_accepted}`, + 'copilot_ide_chat': (data: IdeChatShared) => `Chats: ${data.total_chats}`, + 'copilot_dotcom_chat': (data: DotComChatShared) => `Chats: ${data.total_chats}`, + 'copilot_dotcom_pull_requests': (data: DotComPullRequestsShared) => `Summaries: ${data.total_pr_summaries_created}` + } drilldownSeries.push({ type: 'column', name: new Date(dateData.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', weekday: 'long' }), @@ -51,22 +58,26 @@ export class HighchartsService { { name: 'IDE Completions', y: dateData.copilot_ide_code_completions?.total_engaged_users || 0, - drilldown: `ide_${dateData.date}` + drilldown: `ide_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_ide_code_completions(dateData.copilot_ide_code_completions!) }, { name: 'IDE Chat', y: dateData.copilot_ide_chat?.total_engaged_users || 0, - drilldown: `chat_${dateData.date}` + drilldown: `chat_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_ide_chat(dateData.copilot_ide_chat!) }, { name: 'GitHub.com Chat', y: dateData.copilot_dotcom_chat?.total_engaged_users || 0, - drilldown: `dotcom_chat_${dateData.date}` + drilldown: `dotcom_chat_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_dotcom_chat(dateData.copilot_dotcom_chat!) }, { name: 'Pull Requests', y: dateData.copilot_dotcom_pull_requests?.total_engaged_users || 0, - drilldown: `pr_${dateData.date}` + drilldown: `pr_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_dotcom_pull_requests(dateData.copilot_dotcom_pull_requests!) } ].sort((a, b) => b.y - a.y) }); @@ -79,7 +90,9 @@ export class HighchartsService { data: dateData.copilot_ide_code_completions?.editors.map((editor) => ({ name: editor.name, y: editor.total_engaged_users, - drilldown: `ide_${editor.name}_${dateData.date}` + drilldown: `ide_${editor.name}_${dateData.date}`, + color: this.getEditorColor(editor.name.split('/')[0]), + customTooltipInfo: () => customTooltipInfo.copilot_ide_code_completions(editor) })).sort((a, b) => b.y - a.y) }); @@ -92,23 +105,61 @@ export class HighchartsService { data: editor.models.map((model) => ({ name: model.name, y: model.total_engaged_users, - drilldown: `ide_${editor.name}_${model.name}_${dateData.date}` + drilldown: `ide_${editor.name}_${model.name}_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_ide_code_completions(model) })).sort((a, b) => b.y - a.y) }); // Model languages drilldown editor.models.forEach((model) => { - if ('languages' in model) { + drilldownSeries.push({ + type: 'column', + name: `${model.name} Languages`, + id: `ide_${editor.name}_${model.name}_${dateData.date}`, + data: (model as CodeModel).languages.map((language) => ({ + name: language.name, + y: language.total_engaged_users, + color: this.getLanguageColor(language.name), + drilldown: `lang_${editor.name}_${model.name}_${language.name}_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_ide_code_completions(language) + })).sort((a, b) => b.y - a.y) + }); + // Add acceptance stats drilldown for each language + (model as CodeModel).languages.forEach((language) => { drilldownSeries.push({ type: 'column', - name: `${model.name} Languages`, - id: `ide_${editor.name}_${model.name}_${dateData.date}`, - data: model.languages.map((lang): [string, number] => [ - lang.name, - lang.total_engaged_users - ]).sort((a, b) => b[1] - a[1]) + name: `${language.name} Suggestions`, + id: `lang_${editor.name}_${model.name}_${language.name}_${dateData.date}`, + data: [ + { + name: 'Suggestions', + y: (language.total_code_lines_suggested || 0), + color: '#007ACC' + }, + { + name: 'Accepted', + y: language.total_code_acceptances || 0, + color: '#4CAF50' + }, + { + name: 'LoC Suggested', + y: language.total_code_lines_suggested, + color: '#FFC107' + }, + { + name: 'LoC Accepted', + y: language.total_code_lines_accepted, + color: '#FF5722' + } + ], + tooltip: { + headerFormat: '', + pointFormatter: function () { + return `${this.name}: ${this.y}`; + } + } }); - } + }); }); }); @@ -120,7 +171,9 @@ export class HighchartsService { data: dateData.copilot_ide_chat?.editors?.map((editor) => ({ name: editor.name, y: editor.total_engaged_users, - drilldown: `chat_${editor.name}_${dateData.date}` + drilldown: `chat_${editor.name}_${dateData.date}`, + color: this.getEditorColor(editor.name.split('/')[0]), + customTooltipInfo: () => customTooltipInfo.copilot_ide_chat(editor) })).sort((a, b) => b.y - a.y) }); @@ -133,23 +186,76 @@ export class HighchartsService { data: editor.models.map((model) => ({ name: model.name, y: model.total_engaged_users, + drilldown: `chat_${editor.name}_${model.name}_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_ide_chat(model) })).sort((a, b) => b.y - a.y) }); + (editor.models as ChatModel[]).forEach((model) => { + drilldownSeries.push({ + type: 'column', + name: `${model.name} Suggestions`, + id: `chat_${editor.name}_${model.name}_${dateData.date}`, + data: [ + { + name: 'Chats', + y: model.total_chats, + color: '#007ACC' + }, + { + name: 'Copys', + y: (model.total_chat_copy_events || 0), + color: '#4CAF50' + }, + { + name: 'Inertions', + y: model.total_chat_insertion_events || 0, + color: '#FFC107' + } + ], + tooltip: { + headerFormat: '', + pointFormatter: function () { + return `${this.name}: ${this.y}`; + } + } + }); + }); }); // GitHub.com Chat drilldown (Model level) - drilldownSeries.push({ - type: 'column', - name: 'GitHub.com Chat', - id: `dotcom_chat_${dateData.date}`, - data: dateData.copilot_dotcom_chat?.models?.map((model) => ({ - name: model.name, - y: model.total_engaged_users, - custom: { - totalChats: model.total_chats - } - })).sort((a, b) => b.y - a.y) || [] - }); + if (dateData.copilot_dotcom_chat) { + drilldownSeries.push({ + type: 'column', + name: 'GitHub.com Chat', + id: `dotcom_chat_${dateData.date}`, + data: dateData.copilot_dotcom_chat.models?.map((model) => ({ + name: model.name, + y: model.total_engaged_users, + drilldown: `dotcom_chat_${model.name}_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_dotcom_chat(model) + })).sort((a, b) => b.y - a.y) || [] + }); + dateData.copilot_dotcom_chat.models?.forEach((model) => { + drilldownSeries.push({ + type: 'column', + name: `${model.name} Suggestions`, + id: `dotcom_chat_${model.name}_${dateData.date}`, + data: [ + { + name: 'Chats', + y: model.total_chats, + color: '#007ACC' + } + ], + tooltip: { + headerFormat: '', + pointFormatter: function () { + return `${this.name}: ${this.y}`; + } + } + }); + }); + } // PR drilldown (Repo level) drilldownSeries.push({ @@ -159,7 +265,8 @@ export class HighchartsService { data: dateData.copilot_dotcom_pull_requests?.repositories.map((repo) => ({ name: repo.name || 'Unknown', y: repo.total_engaged_users, - drilldown: `pr_${repo.name}_${dateData.date}` + drilldown: `pr_${repo.name}_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_dotcom_pull_requests(repo) })).sort((a, b) => b.y - a.y) }); @@ -172,12 +279,39 @@ export class HighchartsService { data: repo.models.map((model) => ({ name: model.name, y: model.total_engaged_users, + drilldown: `pr_${repo.name}_${model.name}_${dateData.date}`, + customTooltipInfo: () => customTooltipInfo.copilot_dotcom_pull_requests(model) })).sort((a, b) => b.y - a.y) }); + (repo.models).forEach((model) => { + drilldownSeries.push({ + type: 'column', + name: `${model.name} Suggestions`, + id: `pr_${repo.name}_${model.name}_${dateData.date}`, + data: [ + { + name: 'Created Summaries', + y: model.total_pr_summaries_created, + color: '#007ACC' + } + ], + tooltip: { + headerFormat: '', + pointFormatter: function () { + return `${this.name}: ${this.y}`; + } + } + }); + }); }); }); return { + chart: { + events: { + drilldown: this.drilldownIfSinglePoint() + } + }, series: [engagedUsersSeries], drilldown: { series: drilldownSeries @@ -186,7 +320,11 @@ export class HighchartsService { headerFormat: '{series.name}
', pointFormatter: function (this: CustomHighchartsPoint) { const formatted = this.date ? this.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', weekday: 'short' }) : this.name; - return `${formatted}: ${this.y} users
`; + const parts = [ + `${formatted}: ${this.y} users
` + ] + if (this.customTooltipInfo) parts.push(this.customTooltipInfo()); + return parts.join(''); } } }; @@ -224,9 +362,22 @@ export class HighchartsService { } } }; - + const dateTimes = Object.keys(data); + const isDaily = Math.abs(new Date(dateTimes[1]).getTime() - new Date(dateTimes[0]).getTime()) > 3600000; + const dateFormat = isDaily ? undefined : 'short'; return { series: [activeUsersSeries], + tooltip: { + pointFormatter: (function () { + const parts = [ + `${new DatePipe('en-US').transform(this.x, dateFormat)}
`, + `${this.series.name}: `, + `${this.raw}`, + `(${this.y?.toFixed(1)}%)` + ] + return parts.join(''); + }) as Highcharts.FormatterCallbackFunction + } }; } @@ -355,13 +506,13 @@ export class HighchartsService { pointFormatter: function () { return [ `User: `, - '' + (this as any).raw.userId + '', + '' + this.raw.userId + '', `
Time saved: `, '' + Math.round(this.y || 0) + '%', `
PR: `, - '#' + (this as any).raw.prNumber + '', + '#' + this.raw.prNumber + '', ].join(''); - } + } as Highcharts.FormatterCallbackFunction } }; } @@ -397,7 +548,7 @@ export class HighchartsService { data: seatActivity.reduce((acc, seat, index) => { const lastSeatActivity = seatActivity[index - 1]; - // Skip if same activity timestamp as previous (no new activity) 🕐 + // Skip if same activity timestamp as previous (no new activity) if ( lastSeatActivity?.last_activity_at === seat.last_activity_at && lastSeatActivity?.last_activity_editor === seat.last_activity_editor @@ -405,16 +556,13 @@ export class HighchartsService { return acc; } - const activityTime = new Date(Date.parse(seat.last_activity_at || seat.created_at)); + const activityStartTime = new Date(Date.parse(seat.last_activity_at || seat.created_at)); + const activityEndTime = new Date(Date.parse(seatActivity[index + 1]?.last_activity_at || seatActivity[index + 1]?.created_at)); - // For first activity or new activity timestamp 📊 acc.push({ name: String(seat.assignee?.login || `Seat ${seat.assignee?.id}`), - start: activityTime.getTime(), - // End time is either next activity or current time - end: index < seatActivity.length - 1 - ? new Date(Date.parse(seatActivity[index + 1].last_activity_at || seatActivity[index + 1].created_at)).getTime() - : activityTime.getTime() + (60 * 60 * 1000), // Add 1 hour for last activity + start: activityStartTime.getTime(), + end: index < seatActivity.length - 1 ? activityEndTime.getTime() : activityStartTime.getTime() + (60 * 60 * 1000), y: getEditorIndex(seat), color: getEditorColor(seat), raw: seat, @@ -446,10 +594,21 @@ export class HighchartsService { } transformMetricsToPieDrilldown(metrics: CopilotMetrics): Highcharts.Options { - // Main pie series showing top level categories 🔝 - console.log(metrics) + // eslint-disable-next-line @typescript-eslint/no-explicit-any const sharedSeriesOptions: any = { colorByPoint: true, + innerSize: '40%', + }; + const suggestionsSeries: Highcharts.SeriesOptionsType = { + type: 'pie' as const, + name: 'Copilot Usage', + data: metrics.copilot_ide_code_completions?.editors.map(editor => ({ + name: editor.name, + y: editor.total_code_suggestions, + raw: editor, + drilldown: `editor_${editor.name}`, + color: this.getEditorColor(editor.name.split('/')[0]) + })), dataLabels: [{ enabled: true, distance: 20 @@ -462,18 +621,7 @@ export class HighchartsService { property: 'percentage', value: 10 } - }] - }; - const suggestionsSeries: Highcharts.SeriesOptionsType = { - type: 'pie', - name: 'Copilot Usage', - data: metrics.copilot_ide_code_completions?.editors.map(editor => ({ - name: editor.name, - y: editor.total_code_suggestions, - raw: editor, - drilldown: `editor_${editor.name}`, - color: this.getEditorColor(editor.name.split('/')[0]) - })), + }], ...sharedSeriesOptions }; @@ -488,14 +636,13 @@ export class HighchartsService { id: `editor_${editor.name}`, data: editor.models.map(model => ({ name: model.name, - y: 'languages' in model ? model.total_code_suggestions : model.total_chats, + y: (model as CodeModel).total_code_suggestions, raw: model, - drilldown: 'languages' in model ? `model_${editor.name}_${model.name}` : undefined + drilldown: `model_${editor.name}_${model.name}` })), ...sharedSeriesOptions }); - // Model languages drilldown 📊 editor.models.forEach(model => { if ('languages' in model) { drilldownSeries.push({ @@ -512,9 +659,7 @@ export class HighchartsService { ...sharedSeriesOptions }); - // Language details drilldown 🔍 model.languages.forEach(lang => { - console.log(lang.name) drilldownSeries.push({ type: 'pie', name: `${lang.name} Details`, @@ -524,11 +669,13 @@ export class HighchartsService { name: 'Accepted', y: lang.total_code_acceptances, raw: lang, + color: '#4CAF50' }, { name: 'Ignored', y: (lang.total_code_suggestions || 0) - (lang.total_code_acceptances || 0), raw: lang, + color: '#757575' } ] as CustomHighchartsPoint[], ...sharedSeriesOptions @@ -540,24 +687,28 @@ export class HighchartsService { } return { + chart: { + events: { + drilldown: this.drilldownIfSinglePoint() + } + }, series: [suggestionsSeries], drilldown: { - series: drilldownSeries + series: drilldownSeries, }, tooltip: { headerFormat: undefined, pointFormatter: function () { - const point: any = this; const decimalPipe = new DecimalPipe('en-US'); const parts = [ - `${point.name}
`, - `Suggestions: ${decimalPipe.transform(point.y)}
`, - `Accepted: ${decimalPipe.transform(point.raw.total_code_acceptances)} (${decimalPipe.transform(point.raw.total_code_acceptances / point.y * 100, '1.2-2')}%)
`, - `LoC Suggested: ${decimalPipe.transform(point.raw.total_code_lines_suggested)}
`, - `LoC Accepted: ${decimalPipe.transform(point.raw.total_code_lines_accepted)}
` + `${this.name}
`, + `Suggestions: ${decimalPipe.transform(this.y)}
`, + `Accepted: ${decimalPipe.transform(this.raw.total_code_acceptances)} (${decimalPipe.transform(this.raw.total_code_acceptances / this.raw.total_code_suggestions * 100, '1.2-2')}%)
`, + `LoC Suggested: ${decimalPipe.transform(this.raw.total_code_lines_suggested)}
`, + `LoC Accepted: ${decimalPipe.transform(this.raw.total_code_lines_accepted)}
` ]; return parts.join(''); - } + } as Highcharts.FormatterCallbackFunction } }; } @@ -566,7 +717,7 @@ export class HighchartsService { return ({ 'visualstudio': '#5C2D91', 'vscode': '#007ACC', - 'jetbrains': '#000000', + 'jetbrains': '#FF6B6B', 'xcode': '#157EFB', 'neovim': '#57A143', 'emacs': '#7F5AB6', @@ -634,4 +785,18 @@ export class HighchartsService { return colorMap[language.toLowerCase()] } + + drilldownIfSinglePoint(): Highcharts.DrilldownCallbackFunction { + return function (e) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((e.seriesOptions as any)?.data.length === 1) { + setTimeout(() => { + const point = this.series[0].points[0]; + if (point && point.doDrilldown) { + point.doDrilldown(); + } + }, 0); + } + } + } } diff --git a/frontend/src/app/services/metrics.service.interfaces.ts b/frontend/src/app/services/metrics.service.interfaces.ts index f8980d9..507b676 100644 --- a/frontend/src/app/services/metrics.service.interfaces.ts +++ b/frontend/src/app/services/metrics.service.interfaces.ts @@ -1,82 +1,78 @@ -interface LanguageMetrics { +interface LanguageMetrics extends IdeCodeCompletionsShared { name: string; - total_engaged_users: number; - total_code_suggestions?: number; - total_code_acceptances?: number; - total_code_lines_suggested?: number; - total_code_lines_accepted?: number; } interface ModelBase { name: string; is_custom_model: boolean; custom_model_training_date: string | null; - total_engaged_users: number; } -interface CodeModel extends ModelBase { - total_code_acceptances: number, - total_code_suggestions: number, - total_code_lines_accepted: number, - total_code_lines_suggested: number, +export interface CodeModel extends ModelBase, IdeCodeCompletionsShared { languages: LanguageMetrics[]; } -interface ChatModel extends ModelBase { - total_chats: number; - total_chat_insertion_events?: number; - total_chat_copy_events?: number; +export interface ChatModel extends ModelBase, IdeChatShared { } -interface DotComChatModel extends ModelBase { - total_chats: number; +interface DotComChatModel extends ModelBase, DotComChatShared { } -interface PullRequestModel extends ModelBase { - total_pr_summaries_created: number; +interface PullRequestModel extends ModelBase, DotComPullRequestsShared { } -interface Editor { +interface CodeEditor extends IdeCodeCompletionsShared { name: string; - total_engaged_users: number; - total_code_acceptances: number, - total_code_suggestions: number, - total_code_lines_accepted: number, - total_code_lines_suggested: number, - models: (CodeModel | ChatModel)[]; + models: CodeModel[]; } -interface Repository { +interface ChatEditor extends IdeChatShared { + name: string; + models: ChatModel[]; +} + +interface Repository extends DotComPullRequestsShared { name: string; - total_engaged_users: number; models: PullRequestModel[]; } -interface IdeCodeCompletions { +export interface IdeCodeCompletionsShared { total_engaged_users: number; total_code_acceptances: number; total_code_lines_accepted: number; total_code_lines_suggested: number; total_code_suggestions: number; +} +export interface IdeCodeCompletions extends IdeCodeCompletionsShared { languages: LanguageMetrics[]; - editors: Editor[]; + editors: CodeEditor[]; } -interface IdeChat { +export interface IdeChatShared { + total_engaged_users: number; + total_chats: number; + total_chat_insertion_events?: number; + total_chat_copy_events?: number; +} +export interface IdeChat extends IdeChatShared { total_chats: number; total_engaged_users: number; - editors: Editor[]; + editors: ChatEditor[]; } -interface DotComChat { +export interface DotComChatShared { total_engaged_users: number; total_chats: number; +} +export interface DotComChat extends DotComChatShared { models: DotComChatModel[]; } -interface DotComPullRequests { +export interface DotComPullRequestsShared { total_engaged_users: number; total_pr_summaries_created: number; +} +export interface DotComPullRequests extends DotComPullRequestsShared { repositories: Repository[]; } diff --git a/frontend/src/app/shared/date-range-select/date-range-select.component.ts b/frontend/src/app/shared/date-range-select/date-range-select.component.ts index 8abf8e6..925def7 100644 --- a/frontend/src/app/shared/date-range-select/date-range-select.component.ts +++ b/frontend/src/app/shared/date-range-select/date-range-select.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, Output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { provideNativeDateAdapter } from '@angular/material/core'; import { MatSelectModule } from '@angular/material/select'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -38,7 +38,7 @@ export enum DateRangeOption { templateUrl: './date-range-select.component.html', styleUrl: './date-range-select.component.scss' }) -export class DateRangeSelectComponent implements OnDestroy { +export class DateRangeSelectComponent implements OnInit, OnDestroy { @Output() dateRangeChange = new EventEmitter<{ start: Date, end: Date }>(); private subscriptions: Subscription[] = []; type = new FormControl(); @@ -64,6 +64,7 @@ export class DateRangeSelectComponent implements OnDestroy { this.subscriptions.push( this.range.valueChanges.subscribe(value => { if (value.start && value.end) { + console.log(value); this.dateRangeChange.emit({ start: value.start, end: value.end @@ -73,7 +74,7 @@ export class DateRangeSelectComponent implements OnDestroy { ) } - ngOnInit() { + ngOnInit() { this.dateRangeChange.emit({ start: this.range.value.start, end: this.range.value.end
{point.key}