Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts
5240 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { $ } from '../../../../../base/browser/dom.js';6import { localize } from '../../../../../nls.js';7import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js';8import { chartsBlue, chartsForeground, chartsLines } from '../../../../../platform/theme/common/colorRegistry.js';910export interface ISessionData {11startTime: number;12typedCharacters: number;13aiCharacters: number;14acceptedInlineSuggestions: number | undefined;15chatEditCount: number | undefined;16}1718export interface IDailyAggregate {19date: string; // ISO date string (YYYY-MM-DD)20displayDate: string; // Formatted for display21aiRate: number;22totalAiChars: number;23totalTypedChars: number;24inlineSuggestions: number;25chatEdits: number;26sessionCount: number;27}2829export type ChartViewMode = 'days' | 'sessions';3031export function aggregateSessionsByDay(sessions: readonly ISessionData[]): IDailyAggregate[] {32const dayMap = new Map<string, IDailyAggregate>();3334for (const session of sessions) {35const date = new Date(session.startTime);36const isoDate = date.toISOString().split('T')[0];37const displayDate = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });3839let aggregate = dayMap.get(isoDate);40if (!aggregate) {41aggregate = {42date: isoDate,43displayDate,44aiRate: 0,45totalAiChars: 0,46totalTypedChars: 0,47inlineSuggestions: 0,48chatEdits: 0,49sessionCount: 0,50};51dayMap.set(isoDate, aggregate);52}5354aggregate.totalAiChars += session.aiCharacters;55aggregate.totalTypedChars += session.typedCharacters;56aggregate.inlineSuggestions += session.acceptedInlineSuggestions ?? 0;57aggregate.chatEdits += session.chatEditCount ?? 0;58aggregate.sessionCount += 1;59}6061// Calculate AI rate for each day62for (const aggregate of dayMap.values()) {63const total = aggregate.totalAiChars + aggregate.totalTypedChars;64aggregate.aiRate = total > 0 ? aggregate.totalAiChars / total : 0;65}6667// Sort by date68return Array.from(dayMap.values()).sort((a, b) => a.date.localeCompare(b.date));69}7071export interface IAiStatsChartOptions {72sessions: readonly ISessionData[];73viewMode: ChartViewMode;74}7576export function createAiStatsChart(77options: IAiStatsChartOptions78): HTMLElement {79const { sessions: sessionsData, viewMode: mode } = options;8081const width = 280;82const height = 100;83const margin = { top: 10, right: 10, bottom: 25, left: 30 };84const innerWidth = width - margin.left - margin.right;85const innerHeight = height - margin.top - margin.bottom;8687const container = $('.ai-stats-chart-container');88container.style.position = 'relative';89container.style.marginTop = '8px';9091const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');92svg.setAttribute('width', `${width}px`);93svg.setAttribute('height', `${height}px`);94svg.setAttribute('viewBox', `0 0 ${width} ${height}`);95svg.style.display = 'block';96container.appendChild(svg);9798const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');99g.setAttribute('transform', `translate(${margin.left},${margin.top})`);100svg.appendChild(g);101102if (sessionsData.length === 0) {103const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');104text.setAttribute('x', `${innerWidth / 2}`);105text.setAttribute('y', `${innerHeight / 2}`);106text.setAttribute('text-anchor', 'middle');107text.setAttribute('fill', asCssVariable(chartsForeground));108text.setAttribute('font-size', '11px');109text.textContent = localize('noData', "No data yet");110g.appendChild(text);111return container;112}113114// Draw axes115const xAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');116xAxisLine.setAttribute('x1', '0');117xAxisLine.setAttribute('y1', `${innerHeight}`);118xAxisLine.setAttribute('x2', `${innerWidth}`);119xAxisLine.setAttribute('y2', `${innerHeight}`);120xAxisLine.setAttribute('stroke', asCssVariable(chartsLines));121xAxisLine.setAttribute('stroke-width', '1px');122g.appendChild(xAxisLine);123124const yAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');125yAxisLine.setAttribute('x1', '0');126yAxisLine.setAttribute('y1', '0');127yAxisLine.setAttribute('x2', '0');128yAxisLine.setAttribute('y2', `${innerHeight}`);129yAxisLine.setAttribute('stroke', asCssVariable(chartsLines));130yAxisLine.setAttribute('stroke-width', '1px');131g.appendChild(yAxisLine);132133// Y-axis labels (0%, 50%, 100%)134for (const pct of [0, 50, 100]) {135const y = innerHeight - (pct / 100) * innerHeight;136const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');137label.setAttribute('x', '-4');138label.setAttribute('y', `${y + 3}`);139label.setAttribute('text-anchor', 'end');140label.setAttribute('fill', asCssVariable(chartsForeground));141label.setAttribute('font-size', '9px');142label.textContent = `${pct}%`;143g.appendChild(label);144145if (pct > 0) {146const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');147gridLine.setAttribute('x1', '0');148gridLine.setAttribute('y1', `${y}`);149gridLine.setAttribute('x2', `${innerWidth}`);150gridLine.setAttribute('y2', `${y}`);151gridLine.setAttribute('stroke', asCssVariable(chartsLines));152gridLine.setAttribute('stroke-width', '0.5px');153gridLine.setAttribute('stroke-dasharray', '2,2');154g.appendChild(gridLine);155}156}157158if (mode === 'days') {159renderDaysView();160} else {161renderSessionsView();162}163164function renderDaysView() {165const dailyData = aggregateSessionsByDay(sessionsData);166const barCount = dailyData.length;167const barWidth = Math.min(20, (innerWidth - (barCount - 1) * 2) / barCount);168const gap = 2;169const totalBarSpace = barCount * barWidth + (barCount - 1) * gap;170const startX = (innerWidth - totalBarSpace) / 2;171172// Calculate which labels to show based on available space173// Each label needs roughly 40px of space to not overlap174const minLabelSpacing = 40;175const totalWidth = totalBarSpace;176const maxLabels = Math.max(2, Math.floor(totalWidth / minLabelSpacing));177const labelStep = Math.max(1, Math.ceil(barCount / maxLabels));178179dailyData.forEach((day, i) => {180const x = startX + i * (barWidth + gap);181const barHeight = day.aiRate * innerHeight;182const y = innerHeight - barHeight;183184// Bar for AI rate185const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');186rect.setAttribute('x', `${x}`);187rect.setAttribute('y', `${y}`);188rect.setAttribute('width', `${barWidth}`);189rect.setAttribute('height', `${Math.max(1, barHeight)}`);190rect.setAttribute('fill', asCssVariable(chartsBlue));191rect.setAttribute('rx', '2');192g.appendChild(rect);193194// X-axis label - only show at calculated intervals to avoid overlap195const isFirst = i === 0;196const isLast = i === barCount - 1;197const isAtInterval = i % labelStep === 0;198199if (isFirst || isLast || (isAtInterval && barCount > 2)) {200// Skip middle labels if they would be too close to first/last201if (!isFirst && !isLast) {202const distFromFirst = i * (barWidth + gap);203const distFromLast = (barCount - 1 - i) * (barWidth + gap);204if (distFromFirst < minLabelSpacing || distFromLast < minLabelSpacing) {205return; // Skip this label206}207}208209const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');210label.setAttribute('x', `${x + barWidth / 2}`);211label.setAttribute('y', `${innerHeight + 12}`);212label.setAttribute('text-anchor', 'middle');213label.setAttribute('fill', asCssVariable(chartsForeground));214label.setAttribute('font-size', '8px');215label.textContent = day.displayDate;216g.appendChild(label);217}218});219}220221function renderSessionsView() {222const sessionCount = sessionsData.length;223const barWidth = Math.min(8, (innerWidth - (sessionCount - 1) * 1) / sessionCount);224const gap = 1;225const totalBarSpace = sessionCount * barWidth + (sessionCount - 1) * gap;226const startX = (innerWidth - totalBarSpace) / 2;227228sessionsData.forEach((session, i) => {229const total = session.aiCharacters + session.typedCharacters;230const aiRate = total > 0 ? session.aiCharacters / total : 0;231const x = startX + i * (barWidth + gap);232const barHeight = aiRate * innerHeight;233const y = innerHeight - barHeight;234235// Bar for AI rate236const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');237rect.setAttribute('x', `${x}`);238rect.setAttribute('y', `${y}`);239rect.setAttribute('width', `${barWidth}`);240rect.setAttribute('height', `${Math.max(1, barHeight)}`);241rect.setAttribute('fill', asCssVariable(chartsBlue));242rect.setAttribute('rx', '1');243g.appendChild(rect);244});245246// X-axis labels: only show first and last to avoid overlap247// Each label is roughly 40px wide (e.g., "Jan 15")248const minLabelSpacing = 40;249250if (sessionCount === 0) {251return;252}253254// Always show first label255const firstSession = sessionsData[0];256const firstX = startX;257const firstDate = new Date(firstSession.startTime);258const firstLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');259firstLabel.setAttribute('x', `${firstX + barWidth / 2}`);260firstLabel.setAttribute('y', `${innerHeight + 12}`);261firstLabel.setAttribute('text-anchor', 'start');262firstLabel.setAttribute('fill', asCssVariable(chartsForeground));263firstLabel.setAttribute('font-size', '8px');264firstLabel.textContent = firstDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });265g.appendChild(firstLabel);266267// Show last label if there's enough space and more than 1 session268if (sessionCount > 1 && totalBarSpace >= minLabelSpacing) {269const lastSession = sessionsData[sessionCount - 1];270const lastX = startX + (sessionCount - 1) * (barWidth + gap);271const lastDate = new Date(lastSession.startTime);272const lastLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');273lastLabel.setAttribute('x', `${lastX + barWidth / 2}`);274lastLabel.setAttribute('y', `${innerHeight + 12}`);275lastLabel.setAttribute('text-anchor', 'end');276lastLabel.setAttribute('fill', asCssVariable(chartsForeground));277lastLabel.setAttribute('font-size', '8px');278lastLabel.textContent = lastDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });279g.appendChild(lastLabel);280}281}282283return container;284}285286287