Skip to content

Commit 9b67b1a

Browse files
authored
Merge pull request #123 from github/asizikov/cumulative-aic-chart
Show cumulative AIC usage on overview
2 parents baaef4d + 16f3574 commit 9b67b1a

4 files changed

Lines changed: 124 additions & 55 deletions

File tree

src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,7 @@ function App() {
729729
rangeEnd={rangeEnd}
730730
licenseAmount={licenseAmount}
731731
licenseSeatCounts={licenseSeatCounts}
732+
includedAicCredits={includedAicPoolSize}
732733
reportPlanScope={reportPlanScope}
733734
upgradeRecommendation={individualUpgradeRecommendation}
734735
onAdjustSeatCounts={reportPlanScope === 'organization' && !isIndividualReport ? () => setActiveView('users') : undefined}

src/components/DualAxisLineChart.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Tooltip,
99
Legend,
1010
type ChartOptions,
11+
type LegendItem,
1112
type ScriptableLineSegmentContext,
1213
} from 'chart.js'
1314
import { Line } from 'react-chartjs-2'
@@ -27,12 +28,23 @@ export interface LineSeries {
2728
segmentColor?: (startValue: number, endValue: number) => string
2829
}
2930

31+
export interface ExtraLegendItem {
32+
label: string
33+
color: string
34+
legendOrder?: number
35+
}
36+
3037
export interface DualAxisLineChartProps {
3138
title: string
3239
labels: string[]
3340
series: [LineSeries, LineSeries, ...LineSeries[]]
3441
height?: number
3542
formatYAsCurrency?: boolean
43+
extraLegendItems?: ExtraLegendItem[]
44+
}
45+
46+
type OrderedLegendItem = LegendItem & {
47+
legendOrder?: number
3648
}
3749

3850
function formatTick(value: number): string {
@@ -52,6 +64,7 @@ export function DualAxisLineChart({
5264
series,
5365
height = 320,
5466
formatYAsCurrency = false,
67+
extraLegendItems = [],
5568
}: DualAxisLineChartProps) {
5669
const tickFormatter = formatYAsCurrency ? formatUsdTick : formatTick
5770
const usesSecondaryAxis = series.some((dataset) => dataset.yAxisID === 'y1')
@@ -111,24 +124,33 @@ export function DualAxisLineChart({
111124
font: { size: 11, weight: 500 },
112125
generateLabels: (chart) => {
113126
const defaultLabels = ChartJS.defaults.plugins.legend.labels.generateLabels?.(chart) ?? []
114-
return defaultLabels.map((item) => {
127+
const datasetLabels: OrderedLegendItem[] = defaultLabels.map((item) => {
115128
const dataset = typeof item.datasetIndex === 'number'
116129
? chart.data.datasets[item.datasetIndex] as { legendLabel?: string, legendOrder?: number }
117130
: undefined
118131

119-
return dataset?.legendLabel ? { ...item, text: dataset.legendLabel } : item
120-
}).sort((a, b) => {
121-
const datasetA = typeof a.datasetIndex === 'number'
122-
? chart.data.datasets[a.datasetIndex] as { legendOrder?: number }
123-
: undefined
124-
const datasetB = typeof b.datasetIndex === 'number'
125-
? chart.data.datasets[b.datasetIndex] as { legendOrder?: number }
126-
: undefined
132+
return {
133+
...item,
134+
text: dataset?.legendLabel ?? item.text,
135+
legendOrder: dataset?.legendOrder,
136+
}
137+
})
138+
139+
const additionalLabels: OrderedLegendItem[] = extraLegendItems.map((item) => ({
140+
text: item.label,
141+
fillStyle: item.color,
142+
strokeStyle: item.color,
143+
hidden: false,
144+
lineWidth: 2,
145+
legendOrder: item.legendOrder,
146+
}))
127147

128-
return (datasetA?.legendOrder ?? a.datasetIndex ?? 0) - (datasetB?.legendOrder ?? b.datasetIndex ?? 0)
148+
return [...datasetLabels, ...additionalLabels].sort((a, b) => {
149+
return (a.legendOrder ?? a.datasetIndex ?? 0) - (b.legendOrder ?? b.datasetIndex ?? 0)
129150
})
130151
},
131152
},
153+
onClick: () => undefined,
132154
},
133155
title: {
134156
display: true,

src/views/CostManagementView.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -441,15 +441,6 @@ export function CostManagementView({
441441
: SIMULATED_ADDITIONAL_COLOR
442442
),
443443
},
444-
{
445-
label: 'Simulated - additional usage',
446-
legendOrder: 2,
447-
color: SIMULATED_ADDITIONAL_COLOR,
448-
data: cumulativeSimulationSeries.labels.map(() => null),
449-
yAxisID: 'y',
450-
order: 4,
451-
pointRadius: 0,
452-
},
453444
{
454445
label: 'Included AI Credits pool',
455446
legendOrder: 4,
@@ -461,6 +452,13 @@ export function CostManagementView({
461452
pointRadius: 0,
462453
},
463454
]}
455+
extraLegendItems={[
456+
{
457+
label: 'Simulated - additional usage',
458+
color: SIMULATED_ADDITIONAL_COLOR,
459+
legendOrder: 2,
460+
},
461+
]}
464462
formatYAsCurrency
465463
height={320}
466464
/>

src/views/OverviewView.tsx

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BillingProjectionDisclaimer, BillingTotalsCards } from '../components/u
33
import { appLinks } from '../config/links'
44
import type { ReportPlanScope } from '../pipeline/aicIncludedCredits'
55
import type { DailyUsageData } from '../pipeline/aggregators/dailyUsageAggregator'
6+
import { AIC_UNIT_PRICE_USD } from '../utils/billingConstants'
67
import { fillDataForRange } from '../utils/fillDataForRange'
78
import { formatUsd } from '../utils/format'
89
import type { IndividualPlanUpgradeRecommendation } from '../utils/individualPlanUpgrade'
@@ -18,11 +19,16 @@ type OverviewViewProps = {
1819
business: number
1920
enterprise: number
2021
}
22+
includedAicCredits: number
2123
reportPlanScope?: ReportPlanScope
2224
upgradeRecommendation?: IndividualPlanUpgradeRecommendation | null
2325
onAdjustSeatCounts?: () => void
2426
}
2527

28+
const CURRENT_AIC_COLOR = '#1a7f37'
29+
const ADDITIONAL_AIC_COLOR = '#cf222e'
30+
const INCLUDED_CREDITS_COLOR = '#0969da'
31+
2632
function createEmptyDailyUsage(date: string): DailyUsageData {
2733
return {
2834
date,
@@ -44,6 +50,7 @@ export function OverviewView({
4450
rangeEnd,
4551
licenseAmount,
4652
licenseSeatCounts,
53+
includedAicCredits,
4754
reportPlanScope = 'organization',
4855
upgradeRecommendation = null,
4956
onAdjustSeatCounts,
@@ -73,6 +80,23 @@ export function OverviewView({
7380
const usageBasedBillingDocsUrl = reportPlanScope === 'individual'
7481
? appLinks.usageBasedBillingForIndividualsDocs
7582
: appLinks.usageBasedBillingForOrganizationsDocs
83+
const includedCreditsValue = includedAicCredits * AIC_UNIT_PRICE_USD
84+
const includedCreditsLabel = 'Included value'
85+
const includedCreditsLegendLabel = reportPlanScope === 'individual' ? 'Included AI Credits' : 'Included AI Credits pool'
86+
const includedCreditsDescription = reportPlanScope === 'individual'
87+
? 'Your plan\'s included AI Credits are consumed first. Additional usage spend starts after cumulative AIC gross cost exceeds the included value.'
88+
: 'The account-wide included AI Credits pool is consumed first. Additional usage spend starts after cumulative AIC gross cost exceeds the included value.'
89+
const includedCreditsCardTitle = reportPlanScope === 'individual' ? 'Included credits are coming' : 'Pooled included credits are coming'
90+
const includedCreditsCardBody = reportPlanScope === 'individual'
91+
? 'Under usage-based billing, your Copilot plan includes AI Credits each month. Usage consumes those included credits first; additional usage is billed only after they are used.'
92+
: 'Under usage-based billing, included credits will be pooled across all licensed users in your account. No more unused capacity going to waste from idle users.'
93+
const includedCreditsDocsUrl = reportPlanScope === 'individual'
94+
? appLinks.usageBasedBillingForIndividualsDocs
95+
: appLinks.aiCreditsForOrganizationsDocs
96+
const cumulativeAicGrossAmount = filledDailyUsageData.reduce<number[]>((totals, day) => {
97+
totals.push((totals[totals.length - 1] ?? 0) + day.aicGrossAmount)
98+
return totals
99+
}, [])
76100

77101
return (
78102
<div className="max-w-[var(--width-content-max)] w-full mx-auto px-6 pt-8 pb-12 flex flex-col gap-6">
@@ -146,25 +170,51 @@ export function OverviewView({
146170
<BillingProjectionDisclaimer className="mb-6" />
147171

148172
<section className="grid grid-cols-1 gap-6 w-full">
149-
<DualAxisLineChart
150-
title="Daily Requests & AI Credits"
151-
labels={filledDailyUsageData.map((day) => day.date)}
152-
series={[
153-
{
154-
label: 'Premium Requests',
155-
color: '#6366f1',
156-
data: filledDailyUsageData.map((day) => day.requests),
157-
yAxisID: 'y',
158-
},
159-
{
160-
label: 'AI Credits',
161-
color: '#22c55e',
162-
data: filledDailyUsageData.map((day) => day.aicQuantity),
163-
yAxisID: 'y1',
164-
},
165-
]}
166-
height={320}
167-
/>
173+
<div className="flex flex-col gap-2">
174+
<DualAxisLineChart
175+
title="Cumulative AIC gross cost: included vs additional"
176+
labels={filledDailyUsageData.map((day) => day.date)}
177+
series={[
178+
{
179+
label: 'AIC gross cost',
180+
legendLabel: 'Usage - within included value',
181+
legendOrder: 1,
182+
color: CURRENT_AIC_COLOR,
183+
data: cumulativeAicGrossAmount,
184+
yAxisID: 'y',
185+
order: 1,
186+
segmentColor: (_startValue, endValue) => (
187+
endValue <= includedCreditsValue
188+
? CURRENT_AIC_COLOR
189+
: ADDITIONAL_AIC_COLOR
190+
),
191+
},
192+
{
193+
label: includedCreditsLabel,
194+
legendLabel: includedCreditsLegendLabel,
195+
legendOrder: 3,
196+
color: INCLUDED_CREDITS_COLOR,
197+
data: filledDailyUsageData.map(() => includedCreditsValue),
198+
yAxisID: 'y',
199+
borderDash: [2, 4],
200+
order: 2,
201+
pointRadius: 0,
202+
},
203+
]}
204+
extraLegendItems={[
205+
{
206+
label: 'Usage - additional spend',
207+
color: ADDITIONAL_AIC_COLOR,
208+
legendOrder: 2,
209+
},
210+
]}
211+
formatYAsCurrency
212+
height={320}
213+
/>
214+
<p className="m-0 text-center text-[13px] text-fg-muted leading-normal">
215+
{includedCreditsDescription}
216+
</p>
217+
</div>
168218
<DualAxisLineChart
169219
title="Daily cost: PRU cost vs AIC cost"
170220
labels={filledDailyUsageData.map((day) => day.date)}
@@ -213,24 +263,22 @@ export function OverviewView({
213263
/>
214264
</section>
215265

216-
{reportPlanScope === 'organization' && (
217-
<div className="bg-bg-default border border-border-default rounded-md py-5 px-6 mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5">
218-
<div className="flex-1 flex flex-col gap-1">
219-
<strong className="text-sm font-semibold text-fg-default">Pooled included credits are coming</strong>
220-
<p className="m-0 text-[13px] text-fg-muted leading-normal">
221-
Under usage-based billing, included credits will be pooled across all licensed users in your account. No more unused capacity going to waste from idle users.
222-
</p>
223-
</div>
224-
<a
225-
href={appLinks.aiCreditsForOrganizationsDocs}
226-
className="text-sm font-medium text-fg-accent no-underline whitespace-nowrap hover:underline"
227-
target="_blank"
228-
rel="noopener noreferrer"
229-
>
230-
Learn more &rarr;
231-
</a>
266+
<div className="bg-bg-default border border-border-default rounded-md py-5 px-6 mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5">
267+
<div className="flex-1 flex flex-col gap-1">
268+
<strong className="text-sm font-semibold text-fg-default">{includedCreditsCardTitle}</strong>
269+
<p className="m-0 text-[13px] text-fg-muted leading-normal">
270+
{includedCreditsCardBody}
271+
</p>
232272
</div>
233-
)}
273+
<a
274+
href={includedCreditsDocsUrl}
275+
className="text-sm font-medium text-fg-accent no-underline whitespace-nowrap hover:underline"
276+
target="_blank"
277+
rel="noopener noreferrer"
278+
>
279+
Learn more &rarr;
280+
</a>
281+
</div>
234282

235283
<section className="mt-8">
236284
<h2 className="text-base font-semibold text-fg-default pb-[10px] border-b border-border-default mb-4">Recommended next steps</h2>

0 commit comments

Comments
 (0)