(dailyTokens: DailyModelTokens[], models: string[], terminalWidth: number)
| 938 | xAxisLabels: string; |
| 939 | }; |
| 940 | function generateTokenChart(dailyTokens: DailyModelTokens[], models: string[], terminalWidth: number): ChartOutput | null { |
| 941 | if (dailyTokens.length < 2 || models.length === 0) { |
| 942 | return null; |
| 943 | } |
| 944 | |
| 945 | // Y-axis labels take about 6 characters, plus some padding |
| 946 | // Cap at ~52 to align with heatmap width (1 year of data) |
| 947 | const yAxisWidth = 7; |
| 948 | const availableWidth = terminalWidth - yAxisWidth; |
| 949 | const chartWidth = Math.min(52, Math.max(20, availableWidth)); |
| 950 | |
| 951 | // Distribute data across the available chart width |
| 952 | let recentData: DailyModelTokens[]; |
| 953 | if (dailyTokens.length >= chartWidth) { |
| 954 | // More data than space: take most recent N days |
| 955 | recentData = dailyTokens.slice(-chartWidth); |
| 956 | } else { |
| 957 | // Less data than space: expand by repeating each point |
| 958 | const repeatCount = Math.floor(chartWidth / dailyTokens.length); |
| 959 | recentData = []; |
| 960 | for (const day of dailyTokens) { |
| 961 | for (let i = 0; i < repeatCount; i++) { |
| 962 | recentData.push(day); |
| 963 | } |
| 964 | } |
| 965 | } |
| 966 | |
| 967 | // Color palette for different models - use theme colors |
| 968 | const theme = getTheme(resolveThemeSetting(getGlobalConfig().theme)); |
| 969 | const colors = [themeColorToAnsi(theme.suggestion), themeColorToAnsi(theme.success), themeColorToAnsi(theme.warning)]; |
| 970 | |
| 971 | // Prepare series data for each model |
| 972 | const series: number[][] = []; |
| 973 | const legend: ChartLegend[] = []; |
| 974 | |
| 975 | // Only show top 3 models to keep chart readable |
| 976 | const topModels = models.slice(0, 3); |
| 977 | for (let i = 0; i < topModels.length; i++) { |
| 978 | const model = topModels[i]!; |
| 979 | const data = recentData.map(day => day.tokensByModel[model] || 0); |
| 980 | |
| 981 | // Only include if there's actual data |
| 982 | if (data.some(v => v > 0)) { |
| 983 | series.push(data); |
| 984 | // Use theme colors that match the chart |
| 985 | const bulletColors = [theme.suggestion, theme.success, theme.warning]; |
| 986 | legend.push({ |
| 987 | model: renderModelName(model), |
| 988 | coloredBullet: applyColor(figures.bullet, bulletColors[i % bulletColors.length] as Color) |
| 989 | }); |
| 990 | } |
| 991 | } |
| 992 | if (series.length === 0) { |
| 993 | return null; |
| 994 | } |
| 995 | const chart = asciichart(series, { |
| 996 | height: 8, |
| 997 | colors: colors.slice(0, series.length), |
no test coverage detected