Skip to content

Commit e32131b

Browse files
HduSyclaude
andcommitted
feat: 1-decimal model shares summing to 100% and closed single-model donut
- Tokens-by-model percentages now show 1 decimal, apportioned via the largest-remainder method so visible rows sum to exactly 100.0%. - Whole-number shares drop the ".0" (100% not 100.0%); rows below 0.1% are hidden to avoid a meaningless "0.0%". - Cost donut renders a single model as a closed ring (continuous ring + 1px card-background stroke) instead of an open gapped arc. - Bump version to 0.1.13. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent bdccd6d commit e32131b

5 files changed

Lines changed: 55 additions & 14 deletions

File tree

‎src-tauri/Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src-tauri/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tokenscope"
3-
version = "0.1.12"
3+
version = "0.1.13"
44
description = "Claude CLI token usage dashboard"
55
authors = ["you"]
66
edition = "2021"

‎src-tauri/tauri.conf.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Tokenscope",
4-
"version": "0.1.12",
4+
"version": "0.1.13",
55
"identifier": "com.tokenscope.app",
66
"build": {
77
"beforeDevCommand": "pnpm dev",

‎src/App.tsx‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,26 @@ function Delta({ v, theme }: { v: number; theme: Theme }) {
5555
);
5656
}
5757

58-
function ModelRow({ m, max, theme, total }: { m: ModelStat; max: number; theme: Theme; total: number }) {
58+
// Round each value's share to 1 decimal (%) via largest-remainder apportionment,
59+
// so the displayed percentages sum to exactly 100.0% (plain rounding wouldn't).
60+
function sharePcts(values: number[]): number[] {
61+
const total = values.reduce((s, v) => s + v, 0);
62+
if (total <= 0) return values.map(() => 0);
63+
const UNITS = 1000; // work in 0.1% units; target is 100.0%
64+
const raw = values.map((v) => (v / total) * UNITS);
65+
const units = raw.map(Math.floor);
66+
const left = Math.round(UNITS - units.reduce((s, f) => s + f, 0));
67+
raw
68+
.map((r, i) => ({ i, frac: r - Math.floor(r) }))
69+
.sort((a, b) => b.frac - a.frac)
70+
.slice(0, left)
71+
.forEach(({ i }) => (units[i] += 1));
72+
return units.map((u) => u / 10);
73+
}
74+
75+
function ModelRow({ m, max, theme, share }: { m: ModelStat; max: number; theme: Theme; share: number }) {
76+
// 1-decimal share; whole numbers drop the ".0" (100% not 100.0%).
77+
const pctStr = share % 1 === 0 ? share.toFixed(0) : share.toFixed(1);
5978
return (
6079
<div style={{ display: "flex", alignItems: "center", gap: 9, padding: "5px 0" }}>
6180
<span style={{ width: 7, height: 7, borderRadius: 2, background: m.color, flex: "0 0 auto" }} />
@@ -66,7 +85,7 @@ function ModelRow({ m, max, theme, total }: { m: ModelStat; max: number; theme:
6685
<div style={{ width: `${(m.tokens / max) * 100}%`, height: "100%", background: m.color, borderRadius: 3 }} />
6786
</div>
6887
<span style={{ font: `500 10.5px ${theme.mono}`, color: theme.dim, flex: "0 0 auto", width: 42, textAlign: "right" }}>{fmtTokens(m.tokens)}</span>
69-
<span style={{ font: `600 10.5px ${theme.mono}`, color: theme.text, flex: "0 0 auto", width: 30, textAlign: "right" }}>{pct(m.tokens, total)}%</span>
88+
<span style={{ font: `600 10.5px ${theme.mono}`, color: theme.text, flex: "0 0 auto", width: 40, textAlign: "right" }}>{pctStr}%</span>
7089
</div>
7190
);
7291
}
@@ -149,11 +168,18 @@ function Panel({ dash, dark, onToggleTheme, openGen, active }: { dash: Dashboard
149168
const animTotal = useCountUp(M.totalTokens, `${period}:${openGen}`, active);
150169
const models = P.models;
151170
// Hide noise: 0% token-share rows, and $0 entries in the cost donut.
152-
const tokenModels = models.filter((m) => pct(m.tokens, M.totalTokens || 1) > 0);
171+
// Show models whose share is at least 0.1% when rounded to 1 decimal; below
172+
// that it'd render a meaningless "0.0%" (a negligible token share). Such a
173+
// model can still appear under Cost if it has a non-zero cost.
174+
const tokenModels = models.filter(
175+
(m) => Math.round((m.tokens / (M.totalTokens || 1)) * 1000) / 10 >= 0.1
176+
);
153177
const costModels = models.filter((m) => m.cost > 0);
154178
// models that were used but have no LiteLLM pricing (cost unknown, not $0)
155179
const unpricedModels = models.filter((m) => !m.priced && m.tokens > 0);
156180
const maxM = Math.max(...tokenModels.map((m) => m.tokens), 1e-9);
181+
// Per-row shares that sum to exactly 100.0% (largest-remainder over visible rows).
182+
const tokenShares = sharePcts(tokenModels.map((m) => m.tokens));
157183
const trendSub = { Day: "today 24h", Week: "this week", Month: "this month" }[period];
158184

159185
return (
@@ -216,7 +242,7 @@ function Panel({ dash, dark, onToggleTheme, openGen, active }: { dash: Dashboard
216242
{/* models */}
217243
<div style={{ marginBottom: 4 }}><Label t={t}>Tokens by model</Label></div>
218244
{tokenModels.length === 0 && <div style={{ font: `500 10.5px ${t.mono}`, color: t.faint, padding: "4px 0" }}>No usage in this period</div>}
219-
{tokenModels.map((m, i) => <ModelRow key={i} m={m} max={maxM} theme={t} total={M.totalTokens || 1} />)}
245+
{tokenModels.map((m, i) => <ModelRow key={i} m={m} max={maxM} theme={t} share={tokenShares[i]} />)}
220246
<SectionRule t={t} m="10px 0 10px" />
221247
{/* cost donut */}
222248
<div style={{ marginBottom: 8 }}><Label t={t}>Cost by model</Label></div>

‎src/charts.tsx‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ export function CostDonut({ models, theme, size = 104, thickness = 16 }:
137137
const total = models.reduce((s, m) => s + m.cost, 0) || 1e-9;
138138
const cx = size / 2, cy = size / 2;
139139
const rOut = (size - 2) / 2, rIn = rOut - thickness;
140-
const gap = 0.045;
140+
// Inter-segment gap, only meaningful when there's more than one wedge to
141+
// separate. A single segment is drawn as a closed ring (see render below).
142+
const gap = models.length > 1 ? 0.045 : 0;
141143
let a = -Math.PI / 2;
142144
const arc = (a0: number, a1: number, rO: number, rI: number) => {
143145
const large = a1 - a0 > Math.PI ? 1 : 0;
@@ -163,12 +165,25 @@ export function CostDonut({ models, theme, size = 104, thickness = 16 }:
163165
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
164166
<div style={{ position: "relative", width: size, height: size, flex: "0 0 auto" }}>
165167
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ overflow: "visible" }}>
166-
{wedges.map((w) => (
167-
<path key={w.i} d={w.d} fill={w.m.color}
168-
opacity={hi === -1 || hi === w.i ? 1 : 0.32}
169-
onMouseEnter={() => setHi(w.i)} onMouseLeave={() => setHi(-1)}
170-
style={{ transition: "opacity .14s", cursor: "default" }} />
171-
))}
168+
{models.length === 1 ? (
169+
// Single model: a full closed ring (one 360° arc would be
170+
// degenerate, and the inter-segment gap would leave it open). Trace
171+
// both edges with a 1px card-coloured stroke for a crisp inset look,
172+
// matching the gapped multi-wedge donut.
173+
<g onMouseEnter={() => setHi(0)} onMouseLeave={() => setHi(-1)} style={{ cursor: "default" }}>
174+
<circle cx={cx} cy={cy} r={(rOut + rIn) / 2}
175+
fill="none" stroke={models[0].color} strokeWidth={thickness} />
176+
<circle cx={cx} cy={cy} r={rOut} fill="none" stroke={t.card} strokeWidth={1} />
177+
<circle cx={cx} cy={cy} r={rIn} fill="none" stroke={t.card} strokeWidth={1} />
178+
</g>
179+
) : (
180+
wedges.map((w) => (
181+
<path key={w.i} d={w.d} fill={w.m.color}
182+
opacity={hi === -1 || hi === w.i ? 1 : 0.32}
183+
onMouseEnter={() => setHi(w.i)} onMouseLeave={() => setHi(-1)}
184+
style={{ transition: "opacity .14s", cursor: "default" }} />
185+
))
186+
)}
172187
</svg>
173188
<div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none" }}>
174189
<span style={{ font: `600 ${fit.toFixed(1)}px ${t.mono}`, color: cur ? cur.color : t.text, lineHeight: 1, letterSpacing: "-.01em" }}>{txt}</span>

0 commit comments

Comments
 (0)