Skip to content

Commit 8630c73

Browse files
committed
feat: extend icon/avatar treatment to user/org groupings
Add avatar support to chart legends, tooltips, and hero cards for username/organization/login columns. getGroupIconSvg now returns an <img> tag with the GitHub avatar for user columns. Added columnHasIcons() helper to replace hardcoded sku/product checks.
1 parent 66c5114 commit 8630c73

5 files changed

Lines changed: 31 additions & 15 deletions

File tree

src/components/HeroCardsGrid.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HeroCard } from './HeroCard';
33
import type { ReportSchema } from '../lib/report-schema';
44
import type { ReportSummary, AnyReportRow, TokenUsageRow } from '../lib/types';
55
import { REPORT_TYPES } from '../lib/types';
6-
import { formatCurrency, formatCompact, formatDisplayValue, getGroupIcon } from '../lib/formatters';
6+
import { formatCurrency, formatCompact, formatDisplayValue, getGroupIcon, getAvatarUrl } from '../lib/formatters';
77
import { topN } from '../lib/aggregation';
88
import styles from '../App.module.css';
99

@@ -68,11 +68,17 @@ export function HeroCardsGrid({ schema, summary, visibleRows, reportType }: Hero
6868
3,
6969
);
7070
return top3.map((m) => {
71-
const label = formatDisplayValue(m.key, card.breakdownGroupField!);
72-
const GroupIcon = getGroupIcon(m.key, card.breakdownGroupField!);
71+
const field = card.breakdownGroupField!;
72+
const label = formatDisplayValue(m.key, field);
73+
const GroupIcon = getGroupIcon(m.key, field);
74+
const isAvatar = (field === 'username' || field === 'organization' || field === 'login' || field === 'userLogin') && m.key;
7375
return (
7476
<span key={m.key}>
75-
<span>{GroupIcon && <GroupIcon size={14} />}{label}</span>
77+
<span>
78+
{isAvatar && <img src={getAvatarUrl(m.key, 28)} width={14} height={14} alt="" style={{ borderRadius: '50%', verticalAlign: 'middle', marginRight: 3 }} loading="lazy" />}
79+
{GroupIcon && <GroupIcon size={14} />}
80+
{label}
81+
</span>
7682
<span>{card.format === 'currency' ? formatCurrency(m.value) : formatCompact(m.value)}</span>
7783
</span>
7884
);

src/components/charts/CostBreakdownChart.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ActionList, ActionMenu } from '@primer/react';
55
import { useReport } from '../../context/useReport';
66
import { groupBy, sumBy, timeBucket as bucketRows } from '../../lib/aggregation';
77
import { buildColorMap } from '../../lib/chart-theme';
8-
import { formatDisplayValue, formatCompact, bucketKeyToTimestamp, getGroupIconSvg } from '../../lib/formatters';
8+
import { formatDisplayValue, formatCompact, bucketKeyToTimestamp, getGroupIconSvg, columnHasIcons } from '../../lib/formatters';
99
import type { MetricOption } from '../../lib/report-schema';
1010
import type { AnyReportRow, BillingRow } from '../../lib/types';
1111
import styles from './Charts.module.css';
@@ -53,7 +53,7 @@ export function CostBreakdownChart({ stackField = 'model', metricOptions }: Cost
5353

5454
const seriesColor = colorMap.get(groupInfo.group) ?? '#808fa3';
5555
const displayName = formatDisplayValue(groupInfo.group, stackField) || ' ';
56-
const hasIcons = stackField === 'sku' || stackField === 'product';
56+
const hasIcons = columnHasIcons(stackField);
5757
const iconHtml = hasIcons ? getGroupIconSvg(groupInfo.group, stackField, seriesColor) : '';
5858
return {
5959
type: 'column' as const,
@@ -102,7 +102,7 @@ export function CostBreakdownChart({ stackField = 'model', metricOptions }: Cost
102102
: '<tr style="border-top: 1px solid var(--borderColor-muted, #d1d9e0b3);"><td><b>Total:&nbsp;</b></td><td style="text-align: right;"><b>{point.total:,.0f}</b></td></tr></table>',
103103
},
104104
plotOptions: { column: { stacking: 'normal' } },
105-
legend: stackField === 'sku' || stackField === 'product'
105+
legend: columnHasIcons(stackField)
106106
? { symbolWidth: 0, symbolHeight: 0, symbolPadding: 0 }
107107
: { symbolWidth: 16, symbolHeight: 12, symbolPadding: 5 },
108108
series,

src/components/charts/ModelBreakdownChart.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ActionList, ActionMenu, SegmentedControl } from '@primer/react';
55
import { CopilotIcon, CreditCardIcon } from '@primer/octicons-react';
66
import { useReport } from '../../context/useReport';
77
import { groupBy, sumBy, topN } from '../../lib/aggregation';
8-
import { humanizeColumn, formatCompact, formatDisplayValue, getAvatarUrl, getGroupIconSvg } from '../../lib/formatters';
8+
import { humanizeColumn, formatCompact, formatDisplayValue, getAvatarUrl, getGroupIconSvg, columnHasIcons } from '../../lib/formatters';
99
import { buildColorMap, getModelIconUrl } from '../../lib/chart-theme';
1010
import { REPORT_TYPES } from '../../lib/types';
1111
import type { MetricOption } from '../../lib/report-schema';
@@ -93,7 +93,7 @@ export function GroupBreakdownChart({ stackField = 'model', metricOptions }: Gro
9393

9494
const seriesColor = colorMap.get(stackInfo.stack) ?? '#808fa3';
9595
const displayName = formatDisplayValue(stackInfo.stack, stackField) || ' ';
96-
const hasIcons = stackField === 'sku' || stackField === 'product';
96+
const hasIcons = columnHasIcons(stackField);
9797
const iconHtml = hasIcons ? getGroupIconSvg(stackInfo.stack, stackField, seriesColor) : '';
9898
return {
9999
type: 'bar' as const,
@@ -136,7 +136,7 @@ export function GroupBreakdownChart({ stackField = 'model', metricOptions }: Gro
136136
const name = typeof this.value === 'string' ? this.value : String(this.value);
137137
const isAvatar = groupByColumn === 'username' || groupByColumn === 'organization';
138138
const isModel = groupByColumn === 'model';
139-
const hasIcons = groupByColumn === 'sku' || groupByColumn === 'product';
139+
const hasIcons = columnHasIcons(groupByColumn);
140140
if (isAvatar && name) {
141141
return `<span style="display:inline-flex;align-items:center;gap:6px;">${name}<img src="${getAvatarUrl(name)}" width="16" height="16" style="border-radius:50%;" loading="lazy" /></span>`;
142142
}
@@ -174,7 +174,7 @@ export function GroupBreakdownChart({ stackField = 'model', metricOptions }: Gro
174174
: '<tr style="border-top: 1px solid var(--borderColor-muted, #d1d9e0b3);"><td><b>Total:&nbsp;</b></td><td style="text-align: right;"><b>{point.total:,.0f}</b></td></tr></table>',
175175
},
176176
plotOptions: { bar: { stacking: 'normal' } },
177-
legend: stackField === 'sku' || stackField === 'product'
177+
legend: columnHasIcons(stackField)
178178
? { symbolWidth: 0, symbolHeight: 0, symbolPadding: 0 }
179179
: { symbolWidth: 16, symbolHeight: 12, symbolPadding: 5 },
180180
series,

src/components/charts/TimeSeriesChart.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { HighchartsReact } from 'highcharts-react-official';
44
import { ActionList, ActionMenu, SegmentedControl } from '@primer/react';
55
import { useReport } from '../../context/useReport';
66
import { groupBy, sumBy, timeBucket as bucketRows } from '../../lib/aggregation';
7-
import { humanizeColumn, formatDisplayValue, formatCompact, bucketKeyToTimestamp, getGroupIconSvg } from '../../lib/formatters';
7+
import { humanizeColumn, formatDisplayValue, formatCompact, bucketKeyToTimestamp, getGroupIconSvg, columnHasIcons } from '../../lib/formatters';
88
import { buildColorMap } from '../../lib/chart-theme';
99
import { getStoredValue, setStoredValue, STORAGE_KEYS } from '../../lib/local-storage';
1010
import type { MetricOption } from '../../lib/report-schema';
@@ -134,7 +134,7 @@ export function TimeSeriesChart({ metricOptions }: { metricOptions?: MetricOptio
134134
}
135135

136136
const displayName = formatDisplayValue(group.key, groupByColumn) || ' ';
137-
const hasIcons = groupByColumn === 'sku' || groupByColumn === 'product';
137+
const hasIcons = columnHasIcons(groupByColumn);
138138
const icon = hasIcons ? getGroupIconSvg(group.key, groupByColumn, color) : '';
139139

140140
series.push({
@@ -180,7 +180,7 @@ export function TimeSeriesChart({ metricOptions }: { metricOptions?: MetricOptio
180180
},
181181
},
182182
series,
183-
legend: groupByColumn === 'sku' || groupByColumn === 'product'
183+
legend: columnHasIcons(groupByColumn)
184184
? { symbolWidth: 0, symbolHeight: 0, symbolPadding: 0 }
185185
: { symbolWidth: 16, symbolHeight: 12, symbolPadding: 5 },
186186
chart: { height: 400 },

src/lib/formatters.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,24 @@ export function getProductIconSvg(rawValue: string, color?: string): string {
3737
return renderIcon(icon, color);
3838
}
3939

40+
const AVATAR_COLUMNS = new Set(['username', 'organization', 'login', 'userLogin']);
41+
4042
/** Columns that have dedicated icon functions */
4143
export function getGroupIconSvg(rawValue: string, column: string, color?: string): string {
4244
if (column === 'sku') return getSkuIconSvg(rawValue, color);
4345
if (column === 'product') return getProductIconSvg(rawValue, color);
46+
if (AVATAR_COLUMNS.has(column) && rawValue) {
47+
return `<img src="${getAvatarUrl(rawValue, 32)}" width="16" height="16" style="border-radius:50%;flex-shrink:0;" loading="lazy" />`;
48+
}
4449
return '';
4550
}
4651

47-
/** Get the React icon component for a group value */
52+
/** Check if a column has custom icon/avatar treatment */
53+
export function columnHasIcons(column: string): boolean {
54+
return column === 'sku' || column === 'product' || AVATAR_COLUMNS.has(column);
55+
}
56+
57+
/** Get the React icon component for a group value (SKU/product only, not avatars) */
4858
export function getGroupIcon(rawValue: string, column: string): OcticonComponent | null {
4959
if (column === 'sku') { const i = getSkuIcon(rawValue); return i === TagIcon ? null : i; }
5060
if (column === 'product') { const i = getProductIcon(rawValue); return i === TagIcon ? null : i; }

0 commit comments

Comments
 (0)