Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts
5240 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { $ } from '../../../../../base/browser/dom.js';
7
import { localize } from '../../../../../nls.js';
8
import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js';
9
import { chartsBlue, chartsForeground, chartsLines } from '../../../../../platform/theme/common/colorRegistry.js';
10
11
export interface ISessionData {
12
startTime: number;
13
typedCharacters: number;
14
aiCharacters: number;
15
acceptedInlineSuggestions: number | undefined;
16
chatEditCount: number | undefined;
17
}
18
19
export interface IDailyAggregate {
20
date: string; // ISO date string (YYYY-MM-DD)
21
displayDate: string; // Formatted for display
22
aiRate: number;
23
totalAiChars: number;
24
totalTypedChars: number;
25
inlineSuggestions: number;
26
chatEdits: number;
27
sessionCount: number;
28
}
29
30
export type ChartViewMode = 'days' | 'sessions';
31
32
export function aggregateSessionsByDay(sessions: readonly ISessionData[]): IDailyAggregate[] {
33
const dayMap = new Map<string, IDailyAggregate>();
34
35
for (const session of sessions) {
36
const date = new Date(session.startTime);
37
const isoDate = date.toISOString().split('T')[0];
38
const displayDate = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
39
40
let aggregate = dayMap.get(isoDate);
41
if (!aggregate) {
42
aggregate = {
43
date: isoDate,
44
displayDate,
45
aiRate: 0,
46
totalAiChars: 0,
47
totalTypedChars: 0,
48
inlineSuggestions: 0,
49
chatEdits: 0,
50
sessionCount: 0,
51
};
52
dayMap.set(isoDate, aggregate);
53
}
54
55
aggregate.totalAiChars += session.aiCharacters;
56
aggregate.totalTypedChars += session.typedCharacters;
57
aggregate.inlineSuggestions += session.acceptedInlineSuggestions ?? 0;
58
aggregate.chatEdits += session.chatEditCount ?? 0;
59
aggregate.sessionCount += 1;
60
}
61
62
// Calculate AI rate for each day
63
for (const aggregate of dayMap.values()) {
64
const total = aggregate.totalAiChars + aggregate.totalTypedChars;
65
aggregate.aiRate = total > 0 ? aggregate.totalAiChars / total : 0;
66
}
67
68
// Sort by date
69
return Array.from(dayMap.values()).sort((a, b) => a.date.localeCompare(b.date));
70
}
71
72
export interface IAiStatsChartOptions {
73
sessions: readonly ISessionData[];
74
viewMode: ChartViewMode;
75
}
76
77
export function createAiStatsChart(
78
options: IAiStatsChartOptions
79
): HTMLElement {
80
const { sessions: sessionsData, viewMode: mode } = options;
81
82
const width = 280;
83
const height = 100;
84
const margin = { top: 10, right: 10, bottom: 25, left: 30 };
85
const innerWidth = width - margin.left - margin.right;
86
const innerHeight = height - margin.top - margin.bottom;
87
88
const container = $('.ai-stats-chart-container');
89
container.style.position = 'relative';
90
container.style.marginTop = '8px';
91
92
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
93
svg.setAttribute('width', `${width}px`);
94
svg.setAttribute('height', `${height}px`);
95
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
96
svg.style.display = 'block';
97
container.appendChild(svg);
98
99
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
100
g.setAttribute('transform', `translate(${margin.left},${margin.top})`);
101
svg.appendChild(g);
102
103
if (sessionsData.length === 0) {
104
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
105
text.setAttribute('x', `${innerWidth / 2}`);
106
text.setAttribute('y', `${innerHeight / 2}`);
107
text.setAttribute('text-anchor', 'middle');
108
text.setAttribute('fill', asCssVariable(chartsForeground));
109
text.setAttribute('font-size', '11px');
110
text.textContent = localize('noData', "No data yet");
111
g.appendChild(text);
112
return container;
113
}
114
115
// Draw axes
116
const xAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
117
xAxisLine.setAttribute('x1', '0');
118
xAxisLine.setAttribute('y1', `${innerHeight}`);
119
xAxisLine.setAttribute('x2', `${innerWidth}`);
120
xAxisLine.setAttribute('y2', `${innerHeight}`);
121
xAxisLine.setAttribute('stroke', asCssVariable(chartsLines));
122
xAxisLine.setAttribute('stroke-width', '1px');
123
g.appendChild(xAxisLine);
124
125
const yAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
126
yAxisLine.setAttribute('x1', '0');
127
yAxisLine.setAttribute('y1', '0');
128
yAxisLine.setAttribute('x2', '0');
129
yAxisLine.setAttribute('y2', `${innerHeight}`);
130
yAxisLine.setAttribute('stroke', asCssVariable(chartsLines));
131
yAxisLine.setAttribute('stroke-width', '1px');
132
g.appendChild(yAxisLine);
133
134
// Y-axis labels (0%, 50%, 100%)
135
for (const pct of [0, 50, 100]) {
136
const y = innerHeight - (pct / 100) * innerHeight;
137
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
138
label.setAttribute('x', '-4');
139
label.setAttribute('y', `${y + 3}`);
140
label.setAttribute('text-anchor', 'end');
141
label.setAttribute('fill', asCssVariable(chartsForeground));
142
label.setAttribute('font-size', '9px');
143
label.textContent = `${pct}%`;
144
g.appendChild(label);
145
146
if (pct > 0) {
147
const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
148
gridLine.setAttribute('x1', '0');
149
gridLine.setAttribute('y1', `${y}`);
150
gridLine.setAttribute('x2', `${innerWidth}`);
151
gridLine.setAttribute('y2', `${y}`);
152
gridLine.setAttribute('stroke', asCssVariable(chartsLines));
153
gridLine.setAttribute('stroke-width', '0.5px');
154
gridLine.setAttribute('stroke-dasharray', '2,2');
155
g.appendChild(gridLine);
156
}
157
}
158
159
if (mode === 'days') {
160
renderDaysView();
161
} else {
162
renderSessionsView();
163
}
164
165
function renderDaysView() {
166
const dailyData = aggregateSessionsByDay(sessionsData);
167
const barCount = dailyData.length;
168
const barWidth = Math.min(20, (innerWidth - (barCount - 1) * 2) / barCount);
169
const gap = 2;
170
const totalBarSpace = barCount * barWidth + (barCount - 1) * gap;
171
const startX = (innerWidth - totalBarSpace) / 2;
172
173
// Calculate which labels to show based on available space
174
// Each label needs roughly 40px of space to not overlap
175
const minLabelSpacing = 40;
176
const totalWidth = totalBarSpace;
177
const maxLabels = Math.max(2, Math.floor(totalWidth / minLabelSpacing));
178
const labelStep = Math.max(1, Math.ceil(barCount / maxLabels));
179
180
dailyData.forEach((day, i) => {
181
const x = startX + i * (barWidth + gap);
182
const barHeight = day.aiRate * innerHeight;
183
const y = innerHeight - barHeight;
184
185
// Bar for AI rate
186
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
187
rect.setAttribute('x', `${x}`);
188
rect.setAttribute('y', `${y}`);
189
rect.setAttribute('width', `${barWidth}`);
190
rect.setAttribute('height', `${Math.max(1, barHeight)}`);
191
rect.setAttribute('fill', asCssVariable(chartsBlue));
192
rect.setAttribute('rx', '2');
193
g.appendChild(rect);
194
195
// X-axis label - only show at calculated intervals to avoid overlap
196
const isFirst = i === 0;
197
const isLast = i === barCount - 1;
198
const isAtInterval = i % labelStep === 0;
199
200
if (isFirst || isLast || (isAtInterval && barCount > 2)) {
201
// Skip middle labels if they would be too close to first/last
202
if (!isFirst && !isLast) {
203
const distFromFirst = i * (barWidth + gap);
204
const distFromLast = (barCount - 1 - i) * (barWidth + gap);
205
if (distFromFirst < minLabelSpacing || distFromLast < minLabelSpacing) {
206
return; // Skip this label
207
}
208
}
209
210
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
211
label.setAttribute('x', `${x + barWidth / 2}`);
212
label.setAttribute('y', `${innerHeight + 12}`);
213
label.setAttribute('text-anchor', 'middle');
214
label.setAttribute('fill', asCssVariable(chartsForeground));
215
label.setAttribute('font-size', '8px');
216
label.textContent = day.displayDate;
217
g.appendChild(label);
218
}
219
});
220
}
221
222
function renderSessionsView() {
223
const sessionCount = sessionsData.length;
224
const barWidth = Math.min(8, (innerWidth - (sessionCount - 1) * 1) / sessionCount);
225
const gap = 1;
226
const totalBarSpace = sessionCount * barWidth + (sessionCount - 1) * gap;
227
const startX = (innerWidth - totalBarSpace) / 2;
228
229
sessionsData.forEach((session, i) => {
230
const total = session.aiCharacters + session.typedCharacters;
231
const aiRate = total > 0 ? session.aiCharacters / total : 0;
232
const x = startX + i * (barWidth + gap);
233
const barHeight = aiRate * innerHeight;
234
const y = innerHeight - barHeight;
235
236
// Bar for AI rate
237
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
238
rect.setAttribute('x', `${x}`);
239
rect.setAttribute('y', `${y}`);
240
rect.setAttribute('width', `${barWidth}`);
241
rect.setAttribute('height', `${Math.max(1, barHeight)}`);
242
rect.setAttribute('fill', asCssVariable(chartsBlue));
243
rect.setAttribute('rx', '1');
244
g.appendChild(rect);
245
});
246
247
// X-axis labels: only show first and last to avoid overlap
248
// Each label is roughly 40px wide (e.g., "Jan 15")
249
const minLabelSpacing = 40;
250
251
if (sessionCount === 0) {
252
return;
253
}
254
255
// Always show first label
256
const firstSession = sessionsData[0];
257
const firstX = startX;
258
const firstDate = new Date(firstSession.startTime);
259
const firstLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
260
firstLabel.setAttribute('x', `${firstX + barWidth / 2}`);
261
firstLabel.setAttribute('y', `${innerHeight + 12}`);
262
firstLabel.setAttribute('text-anchor', 'start');
263
firstLabel.setAttribute('fill', asCssVariable(chartsForeground));
264
firstLabel.setAttribute('font-size', '8px');
265
firstLabel.textContent = firstDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
266
g.appendChild(firstLabel);
267
268
// Show last label if there's enough space and more than 1 session
269
if (sessionCount > 1 && totalBarSpace >= minLabelSpacing) {
270
const lastSession = sessionsData[sessionCount - 1];
271
const lastX = startX + (sessionCount - 1) * (barWidth + gap);
272
const lastDate = new Date(lastSession.startTime);
273
const lastLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
274
lastLabel.setAttribute('x', `${lastX + barWidth / 2}`);
275
lastLabel.setAttribute('y', `${innerHeight + 12}`);
276
lastLabel.setAttribute('text-anchor', 'end');
277
lastLabel.setAttribute('fill', asCssVariable(chartsForeground));
278
lastLabel.setAttribute('font-size', '8px');
279
lastLabel.textContent = lastDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
280
g.appendChild(lastLabel);
281
}
282
}
283
284
return container;
285
}
286
287