Skip to content

Commit 40fc2f2

Browse files
argszeroargszero
andauthored
feat(ui): trend chart polish + metric switcher (rant 2026-08-23T16:17:18) (#133)
- Metric switcher (Spend/Income/Net/Tokens, default Spend) with delegated click binding - Smooth Catmull-Rom line, gradient area fill, per-metric theme colors - Adaptive nice-scale axis (1/2/5x10^n; symmetric zero-axis for Net) - Hover tooltip + guide line (replaces <title>), dashed grid, legend total - i18n keys zh+en, cache-bust 20260824-1 Co-authored-by: argszero <argszero@argszerodeMac-mini.local>
1 parent 252f2b7 commit 40fc2f2

4 files changed

Lines changed: 178 additions & 39 deletions

File tree

‎ui/css/style.css‎

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -329,17 +329,60 @@ select.input option { background: var(--bg-card); }
329329
/* 交易趋势图(rant 2026-08-23T16:01:07 需求 2):SVG 折线图,无外部依赖 */
330330
.tx-trend { padding: 10px 16px 8px; border-bottom: 1px solid var(--border); }
331331
.tx-trend-title { color: var(--text-mute); font-size: 12px; margin-bottom: 2px; }
332+
/* 指标切换(rant 2026-08-23T16:17:18 需求 2) */
333+
.tx-trend-switch { display: flex; gap: 6px; margin: 6px 0 8px; flex-wrap: wrap; }
334+
.tsw-btn {
335+
border: 1px solid var(--border);
336+
background: transparent;
337+
color: var(--text-dim);
338+
padding: 3px 12px;
339+
border-radius: 999px;
340+
cursor: pointer;
341+
font-size: 12px;
342+
}
343+
.tsw-btn:hover { color: var(--text); }
344+
.tsw-btn.active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-text); }
345+
.tx-trend-chart { position: relative; }
332346
.tx-trend svg { width: 100%; height: auto; display: block; }
333-
.tx-trend-grid { stroke: var(--border); stroke-width: 1; }
347+
.tx-trend-grid { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 4; }
334348
.tx-trend-axis { fill: var(--text-mute); font-size: 10px; }
335-
.tx-trend svg .tx-trend-inc { fill: none; stroke: var(--ok); stroke-width: 2; }
336-
.tx-trend svg .tx-trend-exp { fill: none; stroke: var(--danger-text); stroke-width: 2; }
337-
.tx-trend svg circle.tx-trend-inc { fill: var(--ok); stroke: none; }
338-
.tx-trend svg circle.tx-trend-exp { fill: var(--danger-text); stroke: none; }
339-
.tx-trend-legend { display: flex; gap: 18px; font-size: 12px; color: var(--text-mute); padding-top: 2px; }
349+
/* 平滑折线 + 渐变面积(指标色随切换,rant 2026-08-23T16:17:18 需求 1) */
350+
.tx-trend svg .trend-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
351+
.tx-trend svg .trend-area { stroke: none; }
352+
.tx-trend svg .trend-dot { fill: var(--bg-card); stroke-width: 1.5; }
353+
.tx-trend svg .tg-0 { stop-opacity: 0; }
354+
.tx-trend svg .tg-1 { stop-opacity: 0; }
355+
.tx-trend svg.m-exp .trend-line { stroke: var(--danger-text); }
356+
.tx-trend svg.m-exp .trend-dot { stroke: var(--danger-text); }
357+
.tx-trend svg.m-exp .tg-0 { stop-color: var(--danger-text); stop-opacity: 0.22; }
358+
.tx-trend svg.m-exp .tg-1 { stop-color: var(--danger-text); }
359+
.tx-trend svg.m-inc .trend-line { stroke: var(--ok); }
360+
.tx-trend svg.m-inc .trend-dot { stroke: var(--ok); }
361+
.tx-trend svg.m-inc .tg-0 { stop-color: var(--ok); stop-opacity: 0.22; }
362+
.tx-trend svg.m-inc .tg-1 { stop-color: var(--ok); }
363+
.tx-trend svg.m-net .trend-line { stroke: var(--accent-text); }
364+
.tx-trend svg.m-net .trend-dot { stroke: var(--accent-text); }
365+
.tx-trend svg.m-net .tg-0 { stop-color: var(--accent-text); stop-opacity: 0.22; }
366+
.tx-trend svg.m-net .tg-1 { stop-color: var(--accent-text); }
367+
.tx-trend svg.m-tok .trend-line { stroke: var(--warn); }
368+
.tx-trend svg.m-tok .trend-dot { stroke: var(--warn); }
369+
.tx-trend svg.m-tok .tg-0 { stop-color: var(--warn); stop-opacity: 0.22; }
370+
.tx-trend svg.m-tok .tg-1 { stop-color: var(--warn); }
371+
/* 悬停引导线 + tooltip */
372+
.tx-trend-guide { position: absolute; top: 2px; bottom: 6px; width: 1px; background: var(--text-dim); opacity: 0.45; display: none; pointer-events: none; }
373+
.tx-trend-tip {
374+
position: absolute; display: none; pointer-events: none;
375+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
376+
padding: 6px 9px; font-size: 12px; color: var(--text);
377+
box-shadow: var(--shadow); z-index: 5; white-space: nowrap; line-height: 1.5;
378+
}
379+
.tx-trend-tip b { color: var(--accent-text); font-weight: 600; }
380+
.tx-trend-legend { display: flex; gap: 18px; font-size: 12px; color: var(--text-mute); padding-top: 4px; align-items: center; }
340381
.tx-trend-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px; vertical-align: middle; }
341-
.tx-trend-dot.inc { background: var(--ok); }
342382
.tx-trend-dot.exp { background: var(--danger-text); }
383+
.tx-trend-dot.inc { background: var(--ok); }
384+
.tx-trend-dot.net { background: var(--accent-text); }
385+
.tx-trend-dot.tok { background: var(--warn); }
343386
.tx-trend-empty { color: var(--text-mute); font-size: 13px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
344387

345388
.table-wrap { overflow-x: auto; }

‎ui/index.html‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>AITokenPool</title>
77
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%234ecdc4'/%3E%3Cstop offset='1' stop-color='%232a9d8f'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='url(%23g)'/%3E%3Ctext x='32' y='43' font-family='-apple-system,Segoe UI,Arial,sans-serif' font-size='30' font-weight='800' text-anchor='middle' fill='%23062021'%3EAT%3C/text%3E%3C/svg%3E">
8-
<link rel="stylesheet" href="css/style.css?v=20260823-3">
8+
<link rel="stylesheet" href="css/style.css?v=20260824-1">
99
</head>
1010
<body>
1111

@@ -683,9 +683,9 @@ <h3 id="chat-title">使用模型</h3>
683683
</div>
684684
</div>
685685

686-
<script src="js/api.js?v=20260823-3"></script>
687-
<script src="js/data.js?v=20260823-3"></script>
688-
<script src="js/i18n.js?v=20260823-3"></script>
689-
<script src="js/app.js?v=20260823-3"></script>
686+
<script src="js/api.js?v=20260824-1"></script>
687+
<script src="js/data.js?v=20260824-1"></script>
688+
<script src="js/i18n.js?v=20260824-1"></script>
689+
<script src="js/app.js?v=20260824-1"></script>
690690
</body>
691691
</html>

‎ui/js/app.js‎

Lines changed: 113 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
let txRange = "24h"; // 交易时间段快捷范围:24h / 7d / 30d / all / custom(默认最近 24 小时,rant 2026-08-22T10:50:00)
1818
let txCustomStart = ""; // 自定义开始(datetime-local 值,本地时区)
1919
let txCustomEnd = ""; // 自定义结束
20+
let txTrendMetric = "expense"; // 趋势图指标(rant 2026-08-23T16:17:18 需求 2):expense / income / net / tokens,默认消费点数
2021
let isGuest = false; // 游客模式(US-1:未登录可浏览市场)
2122
let pendingHashView = null; // URL hash 路由(rant 20:39:30 A):刷新后登录时恢复上次视图
2223
let mkExpanded = null; // 市场行展开(rant 20:39:30 F):当前展开的模型 id,null=全部收起;仅展开当前行
@@ -1240,62 +1241,110 @@
12401241
$("#tx-summary").innerHTML = html;
12411242
}
12421243

1243-
// 交易趋势图(rant 2026-08-23T16:01:07 需求 2):手写 SVG 折线图(无外部依赖),
1244+
// 交易趋势图(rant 2026-08-23T16:01:07 需求 2 + 2026-08-23T16:17:18 优化):手写 SVG(无外部依赖),
12441245
// 数据源 = /api/transactions/trend(跟随 tab + 外部时间段筛选,与 summary 同口径);
1245-
// 横轴 = 时间桶(hour/day/week),纵轴 = 点数;收入(绿) / 支出(红) 两条线,点悬停显示精确值。
1246+
// 指标可切换(消费/收入/净变化/Token,默认消费点数);平滑折线 + 渐变面积 + 自适应刻度 + 悬停 tooltip。
1247+
let _trendCtx = null; // 悬停 tooltip 上下文(renderTxTrend 写入,事件委托读取)
12461248
function renderTxTrend() {
12471249
const el = $("#tx-trend");
12481250
const tr = (Live.transactions && Live.transactions.trend) ? Live.transactions.trend : null;
12491251
const buckets = (tr && Array.isArray(tr.buckets)) ? tr.buckets : [];
12501252
if (!buckets.length) {
12511253
el.innerHTML = '<div class="tx-trend-empty">' + esc(T("tx.trend.empty")) + "</div>";
1254+
_trendCtx = null;
12521255
return;
12531256
}
12541257
const bucket = tr.bucket || "day";
1255-
const income = buckets.map((b) => b.income || 0);
1256-
const expense = buckets.map((b) => b.expense || 0);
1257-
const maxV = Math.max(1, ...income, ...expense);
1258-
const W = 640, H = 170, PL = 46, PR = 12, PT = 12, PB = 24;
1258+
const METRICS = [
1259+
{ key: "expense", label: T("tx.trend.metric.expense"), pick: (b) => b.expense || 0, cls: "exp" },
1260+
{ key: "income", label: T("tx.trend.metric.income"), pick: (b) => b.income || 0, cls: "inc" },
1261+
{ key: "net", label: T("tx.trend.metric.net"), pick: (b) => (b.net === undefined ? (b.income || 0) - (b.expense || 0) : b.net) || 0, cls: "net", signed: true },
1262+
{ key: "tokens", label: T("tx.trend.metric.tokens"), pick: (b) => b.tokens || 0, cls: "tok" },
1263+
];
1264+
const m = METRICS.find((x) => x.key === txTrendMetric) || METRICS[0];
1265+
const vals = buckets.map(m.pick);
1266+
const lbl = (b) => bucketLabel(b.t, bucket);
1267+
const fmtSigned = (n) => (n > 0 ? "+" : n < 0 ? "-" : "") + D.fmt(Math.abs(n));
1268+
const fmtAxis = (n) => {
1269+
const a = Math.abs(n);
1270+
const s = a >= 1e6 ? (a / 1e6).toFixed(2).replace(/\.?0+$/, "") + "M"
1271+
: a >= 1000 ? Math.round(a / 1000) + "K"
1272+
: (Math.round(a * 10) / 10).toString();
1273+
return (n < 0 ? "-" : "") + s;
1274+
};
1275+
// 自适应刻度:nice 上限(1/2/5×10^n);负值指标(净变化)±maxV 对称,零轴居中
1276+
const neg = vals.some((v) => v < 0);
1277+
const absMax = Math.max(0, ...vals.map((v) => Math.abs(v)));
1278+
const np = Math.pow(10, Math.floor(Math.log10(Math.max(absMax, 1e-9))));
1279+
const nm = Math.max(absMax, 1e-9) / np;
1280+
const maxV = Math.max((nm <= 1 ? 1 : nm <= 2 ? 2 : nm <= 5 ? 5 : 10) * np, 4);
1281+
const minV = neg ? -maxV : 0;
1282+
const W = 640, H = 190, PL = 46, PR = 14, PT = 14, PB = 26;
12591283
const iw = W - PL - PR, ih = H - PT - PB;
12601284
const X = (i) => PL + (buckets.length === 1 ? iw / 2 : iw * i / (buckets.length - 1));
1261-
const Y = (v) => PT + ih * (1 - v / maxV);
1262-
const lbl = (b) => bucketLabel(b.t, bucket);
1263-
const fmt = (n) => (n > 0 ? "+" : "") + D.fmt(n);
1264-
const fmtShort = (n) => (n >= 1e6 ? (n / 1e6).toFixed(2) + "M" : (n >= 1000 ? Math.round(n / 1000) + "K" : String(Math.round(n))));
1265-
// 网格 + 纵轴刻度(4 档)
1285+
const Y = (v) => PT + ih * (maxV - v) / (maxV - minV || 1);
1286+
const pts = vals.map((v, i) => [X(i), Y(v)]);
1287+
// 平滑折线(Catmull-Rom → 三次贝塞尔,单点退化为点)
1288+
const smoothPath = (p) => {
1289+
if (!p.length) return "";
1290+
if (p.length === 1) return "M" + p[0][0].toFixed(1) + "," + p[0][1].toFixed(1) + " L" + (p[0][0] + 1).toFixed(1) + "," + p[0][1].toFixed(1);
1291+
let d = "M" + p[0][0].toFixed(1) + "," + p[0][1].toFixed(1);
1292+
for (let i = 0; i < p.length - 1; i++) {
1293+
const p0 = p[Math.max(0, i - 1)], p1 = p[i], p2 = p[i + 1], p3 = p[Math.min(p.length - 1, i + 2)];
1294+
const c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;
1295+
const c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;
1296+
d += " C" + c1x.toFixed(1) + "," + c1y.toFixed(1) + " " + c2x.toFixed(1) + "," + c2y.toFixed(1) + " " + p2[0].toFixed(1) + "," + p2[1].toFixed(1);
1297+
}
1298+
return d;
1299+
};
1300+
// 网格 + 纵轴刻度(4 等分,5 条线,负值指标含负刻度)
12661301
let grid = "";
12671302
for (let g = 0; g <= 4; g++) {
12681303
const gy = PT + ih * g / 4;
1304+
const gv = minV + (maxV - minV) * (4 - g) / 4;
12691305
grid += '<line class="tx-trend-grid" x1="' + PL + '" y1="' + gy.toFixed(1) + '" x2="' + (W - PR) + '" y2="' + gy.toFixed(1) + '"/>' +
1270-
'<text class="tx-trend-axis" x="' + (PL - 6) + '" y="' + (gy + 4).toFixed(1) + '" text-anchor="end">' + fmtShort(maxV * (4 - g) / 4) + "</text>";
1306+
'<text class="tx-trend-axis" x="' + (PL - 6) + '" y="' + (gy + 4).toFixed(1) + '" text-anchor="end">' + fmtAxis(gv) + "</text>";
12711307
}
12721308
// 横轴标签:首 / 中 / 尾
12731309
const idxs = [0, Math.floor((buckets.length - 1) / 2), buckets.length - 1].filter((v, i, a) => a.indexOf(v) === i);
12741310
let xlabels = "";
12751311
idxs.forEach((i) => {
12761312
xlabels += '<text class="tx-trend-axis" x="' + X(i).toFixed(1) + '" y="' + (H - 8) + '" text-anchor="middle">' + esc(lbl(buckets[i])) + "</text>";
12771313
});
1278-
// 折线 + 数据点(<title> 悬停显示精确值)
1279-
const lineHtml = (arr, cls, name) => {
1280-
let pts = "", dots = "";
1281-
arr.forEach((v, i) => {
1282-
const xi = X(i).toFixed(1), yi = Y(v).toFixed(1);
1283-
pts += (i ? " " : "") + xi + "," + yi;
1284-
dots += '<circle class="' + cls + '" cx="' + xi + '" cy="' + yi + '" r="2.5"><title>' +
1285-
esc(lbl(buckets[i]) + " · " + name + " " + fmt(v)) + "</title></circle>";
1314+
// 折线 + 渐变面积 + 数据点(面积基线:负值指标取零轴,正值指标取图表底)
1315+
const yBase = Y(neg ? 0 : minV);
1316+
const areaPath = pts.length
1317+
? smoothPath(pts) + " L" + pts[pts.length - 1][0].toFixed(1) + "," + yBase.toFixed(1) + " L" + pts[0][0].toFixed(1) + "," + yBase.toFixed(1) + " Z"
1318+
: "";
1319+
const lineHtml = () => {
1320+
let dots = "";
1321+
pts.forEach((p) => {
1322+
dots += '<circle class="trend-dot" cx="' + p[0].toFixed(1) + '" cy="' + p[1].toFixed(1) + '" r="2.5"/>';
12861323
});
1287-
return '<polyline class="' + cls + '" points="' + pts + '"/>' + dots;
1324+
return '<path class="trend-area" d="' + areaPath + '"/>' +
1325+
'<path class="trend-line" d="' + smoothPath(pts) + '"/>' + dots;
12881326
};
1327+
const switchHtml =
1328+
'<div class="tx-trend-switch" role="tablist" aria-label="' + esc(T("tx.trend.metricLabel")) + '">' +
1329+
METRICS.map((x) =>
1330+
'<button type="button" class="tsw-btn' + (x.key === m.key ? " active" : "") + '" data-metric="' + x.key + '" role="tab" aria-selected="' + (x.key === m.key) + '">' + esc(x.label) + "</button>"
1331+
).join("") +
1332+
"</div>";
1333+
const total = vals.reduce((a, b) => a + b, 0);
12891334
const html =
12901335
'<div class="tx-trend-title">' + esc(T("tx.trend.title")) + "</div>" +
1291-
'<svg viewBox="0 0 ' + W + " " + H + '" role="img" aria-label="' + esc(T("tx.trend.title")) + '">' +
1292-
grid + xlabels +
1293-
lineHtml(income, "tx-trend-inc", T("tx.summary.income")) +
1294-
lineHtml(expense, "tx-trend-exp", T("tx.summary.expense")) +
1336+
switchHtml +
1337+
'<div class="tx-trend-chart">' +
1338+
'<svg class="m-' + m.cls + '" viewBox="0 0 ' + W + " " + H + '" role="img" aria-label="' + esc(T("tx.trend.title")) + '">' +
1339+
'<defs><linearGradient id="tx-trend-grad" x1="0" y1="0" x2="0" y2="1"><stop class="tg-0" offset="0%"/><stop class="tg-1" offset="100%"/></linearGradient></defs>' +
1340+
grid + xlabels + lineHtml() +
12951341
"</svg>" +
1296-
'<div class="tx-trend-legend"><span><i class="tx-trend-dot inc"></i>' + esc(T("tx.summary.income")) + "</span>" +
1297-
'<span><i class="tx-trend-dot exp"></i>' + esc(T("tx.summary.expense")) + "</span></div>";
1342+
'<div class="tx-trend-guide" id="tx-trend-guide"></div>' +
1343+
'<div class="tx-trend-tip" id="tx-trend-tip"></div>' +
1344+
"</div>" +
1345+
'<div class="tx-trend-legend"><i class="tx-trend-dot ' + m.cls + '"></i><span>' + esc(m.label) + " · " + (m.signed ? fmtSigned(total) : D.fmt(total)) + "</span></div>";
12981346
el.innerHTML = html;
1347+
_trendCtx = { buckets, vals, m, lbl, X, iw, W, PL };
12991348
}
13001349

13011350
// 趋势桶标签:hour → "MM-DD HH:00";day/week → "MM-DD"(bucket 起点均为 UTC,转本地显示)
@@ -3118,6 +3167,43 @@
31183167
showCustom();
31193168
}
31203169

3170+
// 趋势图指标切换 + 悬停 tooltip(rant 2026-08-23T16:17:18 需求 1/2:事件委托,重渲染不丢绑定)
3171+
const txTrendEl = $("#tx-trend");
3172+
if (txTrendEl) {
3173+
txTrendEl.addEventListener("click", (e) => {
3174+
const b = e.target.closest ? e.target.closest(".tsw-btn") : null;
3175+
if (!b || b.dataset.metric === txTrendMetric) return;
3176+
txTrendMetric = b.dataset.metric;
3177+
renderTxTrend();
3178+
});
3179+
txTrendEl.addEventListener("mousemove", (e) => {
3180+
const ctx = _trendCtx;
3181+
const tip = $("#tx-trend-tip"), guide = $("#tx-trend-guide");
3182+
const svg = txTrendEl.querySelector("svg"), chartEl = txTrendEl.querySelector(".tx-trend-chart");
3183+
if (!ctx || !tip || !guide || !svg || !chartEl) return;
3184+
const rect = svg.getBoundingClientRect();
3185+
if (!rect.width) return;
3186+
const vb = svg.getAttribute("viewBox").split(" ").map(Number);
3187+
const x = (e.clientX - rect.left) / rect.width * vb[2];
3188+
const step = ctx.buckets.length > 1 ? ctx.X(1) - ctx.X(0) : ctx.iw;
3189+
const i = Math.max(0, Math.min(ctx.buckets.length - 1, Math.round((x - ctx.PL) / step)));
3190+
const v = ctx.vals[i];
3191+
const cr = chartEl.getBoundingClientRect();
3192+
const valTxt = ctx.m.signed ? (v > 0 ? "+" : v < 0 ? "-" : "") + D.fmt(Math.abs(v)) : D.fmt(v);
3193+
tip.innerHTML = esc(ctx.lbl(ctx.buckets[i])) + "<br><b>" + esc(ctx.m.label) + " " + valTxt + "</b>";
3194+
tip.style.display = "block";
3195+
guide.style.display = "block";
3196+
guide.style.left = (ctx.X(i) / vb[2] * cr.width) + "px";
3197+
tip.style.left = Math.min(Math.max(e.clientX - cr.left + 14, 4), Math.max(cr.width - tip.offsetWidth - 4, 4)) + "px";
3198+
tip.style.top = Math.min(Math.max(e.clientY - cr.top - tip.offsetHeight - 10, 4), Math.max(cr.height - tip.offsetHeight - 4, 4)) + "px";
3199+
});
3200+
txTrendEl.addEventListener("mouseleave", () => {
3201+
const tip = $("#tx-trend-tip"), guide = $("#tx-trend-guide");
3202+
if (tip) tip.style.display = "none";
3203+
if (guide) guide.style.display = "none";
3204+
});
3205+
}
3206+
31213207
// 表格键盘导航(rant 20:46:57 F):点击行 → 激活高亮,之后 ↑/↓/Enter/Esc 可用
31223208
KBD_TABLE_IDS.forEach((id) => {
31233209
const c = document.getElementById(id);

‎ui/js/i18n.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,11 @@
322322
"tx.summary.output": "输出",
323323
"tx.trend.title": "点数趋势",
324324
"tx.trend.empty": "当前时间段暂无趋势数据",
325+
"tx.trend.metricLabel": "趋势图指标",
326+
"tx.trend.metric.expense": "消费",
327+
"tx.trend.metric.income": "收入",
328+
"tx.trend.metric.net": "净变化",
329+
"tx.trend.metric.tokens": "Token",
325330
"tx.col.time": "时间",
326331
"tx.col.type": "类型",
327332
"tx.col.user": "用户",
@@ -1032,6 +1037,11 @@
10321037
"tx.summary.output": "Output",
10331038
"tx.trend.title": "Points Trend",
10341039
"tx.trend.empty": "No trend data in this range",
1040+
"tx.trend.metricLabel": "Trend metric",
1041+
"tx.trend.metric.expense": "Spend",
1042+
"tx.trend.metric.income": "Income",
1043+
"tx.trend.metric.net": "Net",
1044+
"tx.trend.metric.tokens": "Tokens",
10351045
"tx.col.time": "Time",
10361046
"tx.col.type": "Type",
10371047
"tx.col.user": "User",

0 commit comments

Comments
 (0)