| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877 |
- import { dataScheme as vchartDefaultDataScheme } from '@visactor/vchart/esm/theme/color-scheme/builtin/default'
- import { getCurrencyDisplay } from '@/lib/currency'
- import { formatChartTime, type TimeGranularity } from '@/lib/time'
- import { MAX_CHART_TREND_POINTS } from '@/features/dashboard/constants'
- import type {
- QuotaDataItem,
- ProcessedChartData,
- ProcessedUserChartData,
- } from '@/features/dashboard/types'
- type TFunction = (key: string) => string
- function getVChartDefaultColors(domainLength: number) {
- const scheme =
- vchartDefaultDataScheme.find(
- (item) => !item.maxDomainLength || domainLength <= item.maxDomainLength
- ) ?? vchartDefaultDataScheme[vchartDefaultDataScheme.length - 1]
- return scheme.scheme
- }
- function buildModelColorSpec(models: string[]) {
- const domain = Array.from(new Set(models))
- return {
- type: 'ordinal',
- domain,
- range: getVChartDefaultColors(domain.length),
- }
- }
- function renderQuotaCompat(rawQuota: number, digits = 4): string {
- const { config, meta } = getCurrencyDisplay()
- if (meta.kind === 'tokens') return rawQuota.toLocaleString()
- const usd = rawQuota / config.quotaPerUnit
- const rate = 'exchangeRate' in meta ? meta.exchangeRate : 1
- const symbol = 'symbol' in meta ? meta.symbol : '$'
- const value = usd * rate
- const fixed = value.toFixed(digits)
- if (parseFloat(fixed) === 0 && rawQuota > 0 && value > 0) {
- return symbol + Math.pow(10, -digits).toFixed(digits)
- }
- return symbol + fixed
- }
- /**
- * Process and aggregate chart data
- */
- export function processChartData(
- data: QuotaDataItem[],
- timeGranularity: TimeGranularity = 'day',
- t?: TFunction
- ): ProcessedChartData {
- const tt: TFunction = t ?? ((x) => x)
- const otherLabel = tt('Other')
- const formatInt = (value: number) =>
- Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format(value)
- const formatQuotaValue = (value: number) => renderQuotaCompat(value, 4)
- const formatQuotaTotal = (value: number) => renderQuotaCompat(value, 2)
- const MAX_TOOLTIP_MODELS = 15
- const makeTooltipDimensionUpdateContent = () => {
- return (
- array: Array<{
- key: string
- value: string | number
- datum?: Record<string, unknown>
- }>
- ) => {
- array.sort((a, b) => (Number(b.value) || 0) - (Number(a.value) || 0))
- let sum = 0
- for (let i = 0; i < array.length; i++) {
- if (array[i].key === 'Other' || array[i].key === otherLabel) continue
- const v = Number(array[i].value) || 0
- if (
- array[i].datum &&
- (array[i].datum as Record<string, unknown>)?.TimeSum
- ) {
- sum =
- Number((array[i].datum as Record<string, unknown>)?.TimeSum) || sum
- }
- array[i].value = formatQuotaValue(v)
- }
- if (array.length > MAX_TOOLTIP_MODELS) {
- const visible = array.slice(0, MAX_TOOLTIP_MODELS)
- let otherSum = 0
- for (let i = MAX_TOOLTIP_MODELS; i < array.length; i++) {
- const raw = array[i].datum
- ? Number((array[i].datum as Record<string, unknown>)?.rawQuota) || 0
- : 0
- otherSum += raw
- }
- visible.push({
- key: otherLabel,
- value: formatQuotaValue(otherSum),
- })
- array = visible
- }
- array.unshift({
- key: tt('Total:'),
- value: formatQuotaValue(sum),
- })
- return array
- }
- }
- if (!data || data.length === 0) {
- return {
- spec_pie: {
- type: 'pie',
- data: [{ id: 'id0', values: [] }],
- outerRadius: 0.8,
- innerRadius: 0.5,
- padAngle: 0.6,
- valueField: 'value',
- categoryField: 'type',
- title: {
- visible: true,
- text: tt('Call Count Distribution'),
- subtext: tt('No data available'),
- },
- legends: { visible: false },
- label: { visible: false },
- tooltip: {
- mark: {
- content: [],
- },
- },
- },
- spec_line: {
- type: 'bar',
- data: [{ id: 'barData', values: [] }],
- xField: 'Time',
- yField: 'Usage',
- seriesField: 'Model',
- stack: true,
- legends: { visible: true, selectMode: 'single' },
- },
- spec_area: {
- type: 'area',
- data: [{ id: 'areaData', values: [] }],
- xField: 'Time',
- yField: 'Usage',
- seriesField: 'Model',
- stack: true,
- legends: { visible: true, selectMode: 'single' },
- },
- spec_model_line: {
- type: 'line',
- data: [{ id: 'lineData', values: [] }],
- xField: 'Time',
- yField: 'Count',
- seriesField: 'Model',
- legends: { visible: true, selectMode: 'single' },
- title: {
- visible: true,
- text: tt('Call Trend'),
- subtext: `${tt('Total:')} ${formatInt(0)}`,
- },
- },
- spec_rank_bar: {
- type: 'bar',
- data: [{ id: 'rankData', values: [] }],
- xField: 'Model',
- yField: 'Count',
- seriesField: 'Model',
- legends: { visible: true, selectMode: 'single' },
- title: {
- visible: true,
- text: tt('Call Count Ranking'),
- subtext: `${tt('Total:')} ${formatInt(0)}`,
- },
- },
- totalQuotaDisplay: formatQuotaTotal(0),
- }
- }
- const { config } = getCurrencyDisplay()
- const quotaPerUnit = config.quotaPerUnit
- // Aggregate all metrics by time and model
- const timeModelMap = new Map<
- string,
- Map<string, { quota: number; count: number; tokens: number }>
- >()
- const modelTotalsMap = new Map<
- string,
- { quota: number; count: number; tokens: number }
- >()
- data.forEach((item) => {
- const timestamp = Number(item.created_at)
- const timeKey = formatChartTime(timestamp, timeGranularity)
- const model = item.model_name || 'Unknown'
- const quota = Number(item.quota) || 0
- const count = Number(item.count) || 0
- const tokens = Number(item.token_used) || 0
- // Aggregate by time and model
- if (!timeModelMap.has(timeKey)) {
- timeModelMap.set(timeKey, new Map())
- }
- const modelMap = timeModelMap.get(timeKey)!
- const existing = modelMap.get(model) || { quota: 0, count: 0, tokens: 0 }
- modelMap.set(model, {
- quota: existing.quota + quota,
- count: existing.count + count,
- tokens: existing.tokens + tokens,
- })
- // Calculate totals
- const totalExisting = modelTotalsMap.get(model) || {
- quota: 0,
- count: 0,
- tokens: 0,
- }
- modelTotalsMap.set(model, {
- quota: totalExisting.quota + quota,
- count: totalExisting.count + count,
- tokens: totalExisting.tokens + tokens,
- })
- })
- const allModels = Array.from(modelTotalsMap.keys())
- const sortedTimes = Array.from(timeModelMap.keys()).sort()
- const sortedModels = [...allModels].sort()
- const modelColor = buildModelColorSpec([...sortedModels, otherLabel])
- // Pad time points if too few (default 7 points)
- const MAX_TREND_POINTS = MAX_CHART_TREND_POINTS
- const fillTimePoints = (times: string[]) => {
- if (times.length >= MAX_TREND_POINTS) return times
- const lastTime = Math.max(
- ...data.map((item) => Number(item.created_at) || 0)
- )
- const intervalSec =
- timeGranularity === 'week'
- ? 604800
- : timeGranularity === 'day'
- ? 86400
- : 3600
- const padded = Array.from({ length: MAX_TREND_POINTS }, (_, i) =>
- formatChartTime(
- lastTime - (MAX_TREND_POINTS - 1 - i) * intervalSec,
- timeGranularity
- )
- )
- return padded
- }
- const chartTimes = fillTimePoints(sortedTimes)
- const totalTimes = Array.from(modelTotalsMap.values()).reduce(
- (sum, x) => sum + (Number(x.count) || 0),
- 0
- )
- const totalQuotaRaw = Array.from(modelTotalsMap.values()).reduce(
- (sum, x) => sum + (Number(x.quota) || 0),
- 0
- )
- // Pie chart (model call count proportion)
- const pieValues = Array.from(modelTotalsMap.entries())
- .map(([model, stats]) => ({
- type: model,
- value: Number(stats.count) || 0,
- }))
- .sort((a, b) => b.value - a.value)
- // Stacked bar: model quota distribution (quota -> USD)
- const lineValues: Array<{
- Time: string
- Model: string
- rawQuota: number
- Usage: number
- TimeSum: number
- }> = []
- chartTimes.forEach((time) => {
- let timeData = sortedModels.map((model) => {
- const stats = timeModelMap.get(time)?.get(model)
- const rawQuota = Number(stats?.quota) || 0
- const usd = rawQuota ? rawQuota / quotaPerUnit : 0
- // Match legacy frontend getQuotaWithUnit(..., 4)
- const usage = usd ? Number(usd.toFixed(4)) : 0
- return {
- Time: time,
- Model: model,
- rawQuota,
- Usage: usage,
- TimeSum: 0,
- }
- })
- const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0)
- timeData.sort((a, b) => b.rawQuota - a.rawQuota)
- timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum }))
- lineValues.push(...timeData)
- })
- lineValues.sort((a, b) => a.Time.localeCompare(b.Time))
- // Area chart: top models by quota + "Other" bucket (too many series = unreadable)
- const MAX_AREA_MODELS = 15
- const rankedQuotaModels = Array.from(modelTotalsMap.entries())
- .map(([model, stats]) => ({
- Model: model,
- Quota: Number(stats.quota) || 0,
- }))
- .sort((a, b) => b.Quota - a.Quota)
- const topAreaModels = new Set(
- rankedQuotaModels.slice(0, MAX_AREA_MODELS).map((m) => m.Model)
- )
- const areaValues: typeof lineValues = []
- chartTimes.forEach((time) => {
- const buckets = new Map<string, { rawQuota: number; usage: number }>()
- const modelMap = timeModelMap.get(time)
- let timeSum = 0
- sortedModels.forEach((model) => {
- const stats = modelMap?.get(model)
- const rawQuota = Number(stats?.quota) || 0
- const usd = rawQuota ? rawQuota / quotaPerUnit : 0
- const usage = usd ? Number(usd.toFixed(4)) : 0
- timeSum += rawQuota
- const key = topAreaModels.has(model) ? model : otherLabel
- const prev = buckets.get(key) || { rawQuota: 0, usage: 0 }
- buckets.set(key, {
- rawQuota: prev.rawQuota + rawQuota,
- usage: Number((prev.usage + usage).toFixed(4)),
- })
- })
- for (const [model, vals] of buckets) {
- areaValues.push({
- Time: time,
- Model: model,
- rawQuota: vals.rawQuota,
- Usage: vals.usage,
- TimeSum: timeSum,
- })
- }
- })
- areaValues.sort((a, b) => a.Time.localeCompare(b.Time))
- // Line chart: model call trend (top models + "Other" bucket)
- const MAX_TREND_MODELS = 20
- const rankedTrendModels = Array.from(modelTotalsMap.entries())
- .map(([model, stats]) => ({
- Model: model,
- Count: Number(stats.count) || 0,
- }))
- .sort((a, b) => b.Count - a.Count)
- const topTrendModels = rankedTrendModels
- .slice(0, MAX_TREND_MODELS)
- .map((item) => item.Model)
- const otherTrendModels = rankedTrendModels
- .slice(MAX_TREND_MODELS)
- .map((item) => item.Model)
- const modelLineValues: Array<{
- Time: string
- Model: string
- Count: number
- }> = []
- chartTimes.forEach((time) => {
- const timeData = topTrendModels.map((model) => {
- const stats = timeModelMap.get(time)?.get(model)
- return {
- Time: time,
- Model: model,
- Count: Number(stats?.count) || 0,
- }
- })
- if (otherTrendModels.length > 0) {
- const otherCount = otherTrendModels.reduce((sum, model) => {
- const stats = timeModelMap.get(time)?.get(model)
- return sum + (Number(stats?.count) || 0)
- }, 0)
- timeData.push({
- Time: time,
- Model: otherLabel,
- Count: otherCount,
- })
- }
- modelLineValues.push(...timeData)
- })
- modelLineValues.sort((a, b) => a.Time.localeCompare(b.Time))
- // Rank bar: model call count ranking (top 20 + "Other" bucket)
- const MAX_RANK_MODELS = 20
- const allRankValues = Array.from(modelTotalsMap.entries())
- .map(([model, stats]) => ({
- Model: model,
- Count: Number(stats.count) || 0,
- }))
- .sort((a, b) => b.Count - a.Count)
- let rankValues: typeof allRankValues
- if (allRankValues.length > MAX_RANK_MODELS) {
- const topModels = allRankValues.slice(0, MAX_RANK_MODELS)
- const otherCount = allRankValues
- .slice(MAX_RANK_MODELS)
- .reduce((sum, item) => sum + item.Count, 0)
- rankValues = [...topModels, { Model: otherLabel, Count: otherCount }]
- } else {
- rankValues = allRankValues
- }
- return {
- spec_pie: {
- type: 'pie',
- data: [{ id: 'id0', values: pieValues }],
- outerRadius: 0.8,
- innerRadius: 0.5,
- padAngle: 0.6,
- valueField: 'value',
- categoryField: 'type',
- pie: {
- style: { cornerRadius: 10 },
- state: {
- hover: { outerRadius: 0.85, stroke: '#000', lineWidth: 1 },
- selected: { outerRadius: 0.85, stroke: '#000', lineWidth: 1 },
- },
- },
- title: {
- visible: true,
- text: tt('Call Count Distribution'),
- subtext: `${tt('Total:')} ${formatInt(totalTimes)}`,
- },
- legends: { visible: true, orient: 'left' },
- label: { visible: true },
- color: modelColor,
- tooltip: {
- mark: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.type,
- value: (datum: Record<string, unknown>) =>
- formatInt(Number(datum?.value) || 0),
- },
- ],
- },
- },
- background: { fill: 'transparent' },
- animation: true,
- },
- spec_line: {
- type: 'bar',
- data: [{ id: 'barData', values: lineValues }],
- xField: 'Time',
- yField: 'Usage',
- seriesField: 'Model',
- stack: true,
- legends: { visible: true, selectMode: 'single' },
- color: modelColor,
- bar: {
- state: {
- hover: { stroke: '#000', lineWidth: 1 },
- },
- },
- tooltip: {
- mark: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.Model,
- value: (datum: Record<string, unknown>) =>
- formatQuotaValue(Number(datum?.rawQuota) || 0),
- },
- ],
- },
- dimension: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.Model,
- value: (datum: Record<string, unknown>) =>
- Number(datum?.rawQuota) || 0,
- },
- ],
- updateContent: makeTooltipDimensionUpdateContent(),
- },
- },
- background: { fill: 'transparent' },
- animation: true,
- },
- spec_area: {
- type: 'area',
- data: [{ id: 'areaData', values: areaValues }],
- xField: 'Time',
- yField: 'Usage',
- seriesField: 'Model',
- stack: false,
- legends: { visible: true, selectMode: 'single' },
- color: modelColor,
- tooltip: {
- mark: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.Model,
- value: (datum: Record<string, unknown>) =>
- formatQuotaValue(Number(datum?.rawQuota) || 0),
- },
- ],
- },
- dimension: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.Model,
- value: (datum: Record<string, unknown>) =>
- Number(datum?.rawQuota) || 0,
- },
- ],
- updateContent: makeTooltipDimensionUpdateContent(),
- },
- },
- area: {
- style: {
- fillOpacity: 0.08,
- curveType: 'monotone',
- },
- },
- line: {
- style: {
- lineWidth: 2,
- curveType: 'monotone',
- },
- },
- point: { visible: false },
- background: { fill: 'transparent' },
- animation: true,
- },
- spec_model_line: {
- type: 'area',
- data: [{ id: 'lineData', values: modelLineValues }],
- xField: 'Time',
- yField: 'Count',
- seriesField: 'Model',
- stack: false,
- legends: { visible: true, selectMode: 'single' },
- color: modelColor,
- title: {
- visible: true,
- text: tt('Call Trend'),
- subtext: `${tt('Total:')} ${formatInt(totalTimes)}`,
- },
- tooltip: {
- mark: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.Model,
- value: (datum: Record<string, unknown>) =>
- formatInt(Number(datum?.Count) || 0),
- },
- ],
- },
- dimension: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.Model,
- value: (datum: Record<string, unknown>) =>
- Number(datum?.Count) || 0,
- },
- ],
- updateContent: (
- array: Array<{
- key: string
- value: string | number
- }>
- ) => {
- array.sort(
- (a, b) => (Number(b.value) || 0) - (Number(a.value) || 0)
- )
- let sum = 0
- for (let i = 0; i < array.length; i++) {
- const v = Number(array[i].value) || 0
- sum += v
- array[i].value = formatInt(v)
- }
- array.unshift({
- key: tt('Total:'),
- value: formatInt(sum),
- })
- return array
- },
- },
- },
- area: {
- style: {
- fillOpacity: 0.08,
- curveType: 'monotone',
- },
- },
- line: {
- style: {
- lineWidth: 2,
- curveType: 'monotone',
- },
- },
- point: { visible: false },
- background: { fill: 'transparent' },
- animation: true,
- },
- spec_rank_bar: {
- type: 'bar',
- data: [{ id: 'rankData', values: rankValues }],
- xField: 'Model',
- yField: 'Count',
- seriesField: 'Model',
- legends: { visible: true, selectMode: 'single' },
- color: modelColor,
- title: {
- visible: true,
- text: tt('Call Count Ranking'),
- subtext: `${tt('Total:')} ${formatInt(totalTimes)}`,
- },
- bar: {
- state: {
- hover: { stroke: '#000', lineWidth: 1 },
- },
- },
- tooltip: {
- mark: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.Model,
- value: (datum: Record<string, unknown>) =>
- formatInt(Number(datum?.Count) || 0),
- },
- ],
- },
- },
- background: { fill: 'transparent' },
- animation: true,
- },
- totalQuotaDisplay: formatQuotaTotal(totalQuotaRaw),
- }
- }
- const USER_COLORS = [
- '#5B8FF9',
- '#5AD8A6',
- '#F6BD16',
- '#E8684A',
- '#6DC8EC',
- '#9270CA',
- '#FF9D4D',
- '#269A99',
- '#FF99C3',
- '#5D7092',
- ]
- export function processUserChartData(
- data: QuotaDataItem[],
- timeGranularity: TimeGranularity = 'day',
- t?: TFunction,
- limit = 10
- ): ProcessedUserChartData {
- const tt: TFunction = t ?? ((x) => x)
- const { config } = getCurrencyDisplay()
- const quotaPerUnit = config.quotaPerUnit
- const formatVal = (raw: number) => renderQuotaCompat(raw, 2)
- const emptyResult: ProcessedUserChartData = {
- spec_user_rank: {
- type: 'bar',
- data: [{ id: 'userRankData', values: [] }],
- xField: 'rawQuota',
- yField: 'User',
- seriesField: 'User',
- direction: 'horizontal',
- title: {
- visible: true,
- text: tt('User Consumption Ranking'),
- subtext: tt('No data available'),
- },
- legends: { visible: false },
- color: { type: 'ordinal', range: USER_COLORS },
- background: { fill: 'transparent' },
- },
- spec_user_trend: {
- type: 'area',
- data: [{ id: 'userTrendData', values: [] }],
- xField: 'Time',
- yField: 'rawQuota',
- seriesField: 'User',
- title: {
- visible: true,
- text: tt('User Consumption Trend'),
- subtext: tt('No data available'),
- },
- legends: { visible: true, selectMode: 'single' },
- color: { type: 'ordinal', range: USER_COLORS },
- point: { visible: false },
- background: { fill: 'transparent' },
- },
- }
- if (!data || data.length === 0) return emptyResult
- const userQuotaTotal = new Map<string, number>()
- data.forEach((item) => {
- const username = item.username || 'unknown'
- const prev = userQuotaTotal.get(username) || 0
- userQuotaTotal.set(username, prev + (Number(item.quota) || 0))
- })
- const sorted = Array.from(userQuotaTotal.entries()).sort(
- (a, b) => b[1] - a[1]
- )
- const topUsers = sorted.slice(0, limit).map(([u]) => u)
- const topUserSet = new Set(topUsers)
- const totalQuota = sorted.slice(0, limit).reduce((s, [, q]) => s + q, 0)
- const rankValues = sorted.slice(0, limit).map(([username, quota]) => ({
- User: username,
- rawQuota: quota,
- Usage: Number((quota / quotaPerUnit).toFixed(4)),
- }))
- const userColorMap = topUsers.reduce<Record<string, string>>(
- (acc, user, i) => {
- acc[user] = USER_COLORS[i % USER_COLORS.length]
- return acc
- },
- {}
- )
- const timeUserMap = new Map<string, Map<string, number>>()
- const allTimePoints = new Set<string>()
- data.forEach((item) => {
- const ts = Number(item.created_at)
- const timeKey = formatChartTime(ts, timeGranularity)
- allTimePoints.add(timeKey)
- const user = item.username || 'unknown'
- if (!topUserSet.has(user)) return
- if (!timeUserMap.has(timeKey)) timeUserMap.set(timeKey, new Map())
- const map = timeUserMap.get(timeKey)!
- map.set(user, (map.get(user) || 0) + (Number(item.quota) || 0))
- })
- const sortedTimePoints = Array.from(allTimePoints).sort()
- const trendValues: Array<{
- Time: string
- User: string
- rawQuota: number
- Usage: number
- }> = []
- sortedTimePoints.forEach((time) => {
- topUsers.forEach((user) => {
- const q = timeUserMap.get(time)?.get(user) || 0
- trendValues.push({
- Time: time,
- User: user,
- rawQuota: q,
- Usage: Number((q / quotaPerUnit).toFixed(4)),
- })
- })
- })
- return {
- spec_user_rank: {
- type: 'bar',
- data: [{ id: 'userRankData', values: rankValues }],
- xField: 'rawQuota',
- yField: 'User',
- seriesField: 'User',
- direction: 'horizontal',
- title: {
- visible: true,
- text: tt('User Consumption Ranking'),
- subtext: `${tt('Total:')} ${formatVal(totalQuota)}`,
- },
- legends: { visible: false },
- bar: {
- state: { hover: { stroke: '#000', lineWidth: 1 } },
- },
- label: {
- visible: true,
- position: 'outside',
- formatMethod: (value: number) => formatVal(value),
- style: { fontSize: 11 },
- },
- axes: [
- { orient: 'left', type: 'band' },
- { orient: 'bottom', type: 'linear', visible: false },
- ],
- tooltip: {
- mark: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.User,
- value: (datum: Record<string, unknown>) =>
- formatVal(Number(datum?.rawQuota) || 0),
- },
- ],
- },
- },
- color: { specified: userColorMap },
- background: { fill: 'transparent' },
- animation: true,
- },
- spec_user_trend: {
- type: 'area',
- data: [{ id: 'userTrendData', values: trendValues }],
- xField: 'Time',
- yField: 'rawQuota',
- seriesField: 'User',
- stack: false,
- title: {
- visible: true,
- text: tt('User Consumption Trend'),
- subtext: `${tt('Total:')} ${formatVal(totalQuota)}`,
- },
- legends: { visible: true, selectMode: 'single' },
- axes: [
- { orient: 'bottom', type: 'band' },
- {
- orient: 'left',
- type: 'linear',
- label: {
- formatMethod: (value: number) => formatVal(value),
- },
- },
- ],
- tooltip: {
- mark: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.User,
- value: (datum: Record<string, unknown>) =>
- formatVal(Number(datum?.rawQuota) || 0),
- },
- ],
- },
- dimension: {
- content: [
- {
- key: (datum: Record<string, unknown>) => datum?.User,
- value: (datum: Record<string, unknown>) =>
- Number(datum?.rawQuota) || 0,
- },
- ],
- updateContent: (
- array: Array<{
- key: string
- value: string | number
- }>
- ) => {
- array.sort(
- (a, b) => (Number(b.value) || 0) - (Number(a.value) || 0)
- )
- let sum = 0
- for (let i = 0; i < array.length; i++) {
- const v = Number(array[i].value) || 0
- sum += v
- array[i].value = formatVal(v)
- }
- array.unshift({
- key: tt('Total:'),
- value: formatVal(sum),
- })
- return array
- },
- },
- },
- area: { style: { fillOpacity: 0.15 } },
- line: { style: { lineWidth: 2 } },
- point: { visible: false },
- color: { specified: userColorMap },
- background: { fill: 'transparent' },
- animation: true,
- },
- }
- }
|