diff --git a/package.json b/package.json index 5b922ab4..087b2291 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "classnames": "^2.5.1", "clipboard": "^2.0.11", "crypto-js": "^4.2.0", + "culori": "^4.0.2", "dayjs": "^1.11.11", "dompurify": "^3.2.6", "driver.js": "^1.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61278d34..9a71e25d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: crypto-js: specifier: ^4.2.0 version: 4.2.0 + culori: + specifier: ^4.0.2 + version: 4.0.2 dayjs: specifier: ^1.11.11 version: 1.11.20 @@ -3810,6 +3813,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==, tarball: https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz} + culori@4.0.2: + resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==, tarball: https://registry.npmjs.org/culori/-/culori-4.0.2.tgz} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + current-script-polyfill@1.0.0: resolution: {integrity: sha512-qv8s+G47V6Hq+g2kRE5th+ASzzrL7b6l+tap1DHKK25ZQJv3yIFhH96XaQ7NGL+zRW3t/RDbweJf/dJDe5Z5KA==, tarball: https://registry.npmjs.org/current-script-polyfill/-/current-script-polyfill-1.0.0.tgz} @@ -13699,6 +13706,8 @@ snapshots: csstype@3.2.3: {} + culori@4.0.2: {} + current-script-polyfill@1.0.0: {} d@1.0.2: diff --git a/src/hooks/use-cool-colors.ts b/src/hooks/use-cool-colors.ts new file mode 100644 index 00000000..874a7886 --- /dev/null +++ b/src/hooks/use-cool-colors.ts @@ -0,0 +1,56 @@ +import useMemoizedFn from 'ahooks/lib/useMemoizedFn'; +import { converter, modeHsl, modeRgb, useMode } from 'culori/fn'; + +const toHsl = converter('hsl'); + +const DEFAULT_BRAND_HUE = 211; // brand color +const COOL_HUE_START = 180; +const COOL_HUE_END = 280; +const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START; +const GOLDEN_RATIO_CONJUGATE = 0.618033988749895; + +function colorStringToHue(input?: string): number | undefined { + if (!input) return undefined; + const color = toHsl(input); + if (!color || typeof color.h !== 'number') return undefined; + return color.h; +} + +export default function useCoolColors() { + // const brandHue = useMemo( + // () => colorStringToHue(userSettings.colorPrimary) ?? DEFAULT_BRAND_HUE, + // [userSettings.colorPrimary] + // ); + useMode(modeRgb); + useMode(modeHsl); + + const brandHue = DEFAULT_BRAND_HUE; + + return useMemoizedFn((count: number): string[] => { + if (count <= 0) return []; + + const colors: string[] = []; + + for (let i = 0; i < count; i++) { + let hue: number; + + if (i === 0) { + hue = brandHue - 5; + } else { + const offset = (i * GOLDEN_RATIO_CONJUGATE) % 1; + hue = COOL_HUE_START + offset * COOL_HUE_RANGE; + + if (Math.abs(hue - brandHue) < 5) { + hue = (hue + 10) % COOL_HUE_END; + } + } + + const s = i === 0 ? 100 : 75 + (i % 3) * 5; + const l = i === 0 ? 50 : 55 + (i % 2) * 5; + + colors.push(`hsl(${Math.round(hue)}, ${s}%, ${l}%)`); + } + + return colors; + }); +} diff --git a/src/pages/_components/bar-chart/index.tsx b/src/pages/_components/bar-chart/index.tsx index d772732e..3196fbd0 100644 --- a/src/pages/_components/bar-chart/index.tsx +++ b/src/pages/_components/bar-chart/index.tsx @@ -1,27 +1,10 @@ +import useCoolColors from '@/hooks/use-cool-colors'; import { Chart } from '@gpustack/core-ui'; import { formatLargeNumber } from '@gpustack/core-ui/utils'; import { theme } from 'antd'; import _ from 'lodash'; import React, { useEffect, useMemo, useRef } from 'react'; -const COOL_HUE_START = 180; -const COOL_HUE_END = 280; -const COOL_HUE_RANGE = COOL_HUE_END - COOL_HUE_START; -// golden angle (137.508) mod COOL_HUE_RANGE — coprime offset for even spread -const HUE_STEP = 37.508; - -export function generateCoolColors(count: number): string[] { - if (count <= 0) return []; - const colors: string[] = []; - for (let i = 0; i < count; i++) { - const hue = COOL_HUE_START + ((i * HUE_STEP) % COOL_HUE_RANGE); - const saturation = 55 + (i % 3) * 5; - const lightness = 60 + (i % 2) * 6; - colors.push(`hsl(${Math.round(hue)}, ${saturation}%, ${lightness}%)`); - } - return colors; -} - export interface BarSeriesItem { name: string; data: any[]; @@ -55,10 +38,11 @@ const BarChart: React.FC = (props) => { } = props; const { token } = theme.useToken(); const chartRef = useRef<{ chart: any } | null>(null); + const generateCoolColors = useCoolColors(); const dynamicColors = useMemo( () => generateCoolColors(seriesData.length), - [seriesData.length] + [seriesData.length, generateCoolColors] ); const options = useMemo(() => { diff --git a/src/pages/_components/pie-chart/index.tsx b/src/pages/_components/pie-chart/index.tsx index c1ac9476..83acfcf5 100644 --- a/src/pages/_components/pie-chart/index.tsx +++ b/src/pages/_components/pie-chart/index.tsx @@ -1,8 +1,8 @@ +import useCoolColors from '@/hooks/use-cool-colors'; import { Chart } from '@gpustack/core-ui'; import { formatLargeNumber } from '@gpustack/core-ui/utils'; import { Empty, Spin, theme } from 'antd'; import React, { useMemo, useRef } from 'react'; -import { generateCoolColors } from '../bar-chart'; export interface PieChartItem { name: string; @@ -30,11 +30,12 @@ const PieChart: React.FC = ({ }) => { const { token } = theme.useToken(); const chartRef = useRef<{ chart: any } | null>(null); + const generateCoolColors = useCoolColors(); const colors = useMemo(() => { const generatedColors = generateCoolColors(data.length + colorOffset); return generatedColors.slice(colorOffset); - }, [colorOffset, data.length]); + }, [colorOffset, data.length, generateCoolColors]); const options = useMemo( () => ({ diff --git a/src/pages/dashboard/components/new-usage.tsx b/src/pages/dashboard/components/new-usage.tsx index 5ad23ae1..9a075b91 100644 --- a/src/pages/dashboard/components/new-usage.tsx +++ b/src/pages/dashboard/components/new-usage.tsx @@ -1,20 +1,12 @@ -import { activeModelsAtom } from '@/atoms/dashboard'; import useQueryBreakdownList from '@/pages/usage/services/use-query-breakdown-list'; import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data'; -import { formatLargeNumber } from '@/utils'; import { PageTools } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Col, Row } from 'antd'; import dayjs from 'dayjs'; -import { useAtomValue } from 'jotai'; -import { useContext, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import styled from 'styled-components'; -import { - baseColorMap, - DashboardUsageCommonParams, - getUsageSummary -} from '../config'; -import { DashboardContext } from '../config/dashboard-context'; +import { DashboardUsageCommonParams } from '../config'; import ApiRequestsByModel from './usage-charts/api-requests-by-model'; import TokenUsageByModel from './usage-charts/token-usage-by-model'; import TopTokenUsageByApiKey from './usage-charts/top-token-usage-by-api-key'; @@ -26,9 +18,6 @@ const Section = styled.div` const NewUsage = () => { const intl = useIntl(); - const activeModelsFromAtom = useAtomValue(activeModelsAtom); - const { active_models: activeModelsFromContext = [] } = - useContext(DashboardContext); const tokenByModelQuery = useQueryTimeSeriesData({ key: 'tokenUsageByModelData' }); @@ -54,10 +43,6 @@ const NewUsage = () => { [dateRange] ); - const activeModels = activeModelsFromAtom.length - ? activeModelsFromAtom - : activeModelsFromContext; - useEffect(() => { const fetchData = async () => { await Promise.all([ @@ -66,15 +51,6 @@ const NewUsage = () => { metric: 'total_tokens', group_by: ['date', 'model'], sort_by: '-total_tokens' - }), - activeUsersQuery.fetchData({ - ...dateRange, - scope: 'all', - group_by: ['user'], - page: 1, - perPage: 10, - sort_by: '-total_tokens', - filters: {} }) ]); }; @@ -82,41 +58,6 @@ const NewUsage = () => { fetchData().catch(() => {}); }, [commonParams, dateRange]); - const summaryCards = useMemo(() => { - const summary = getUsageSummary(tokenByModelQuery.detailData) || { - total_tokens: 0, - api_requests: 0 - }; - - return [ - { - label: formatLargeNumber(summary.total_tokens) as string, - value: intl.formatMessage({ id: 'dashboard.tokens' }), - color: baseColorMap.base - }, - { - label: formatLargeNumber(summary.api_requests) as string, - value: intl.formatMessage({ id: 'dashboard.apirequest' }), - color: baseColorMap.baseR1 - }, - { - label: formatLargeNumber(activeModels.length) as string, - value: intl.formatMessage({ id: 'dashboard.activeModels' }), - color: baseColorMap.baseR2 - }, - { - label: formatLargeNumber(activeUsersQuery.dataSource.total) as string, - value: intl.formatMessage({ id: 'dashboard.activeUsers' }), - color: baseColorMap.baseR3 - } - ]; - }, [ - activeModels.length, - activeUsersQuery.dataSource.total, - intl, - tokenByModelQuery.detailData - ]); - return ( <> {
- + - +
diff --git a/src/pages/dashboard/components/usage-charts/api-requests-by-model.tsx b/src/pages/dashboard/components/usage-charts/api-requests-by-model.tsx index 4ad486c8..56548519 100644 --- a/src/pages/dashboard/components/usage-charts/api-requests-by-model.tsx +++ b/src/pages/dashboard/components/usage-charts/api-requests-by-model.tsx @@ -53,7 +53,7 @@ const ApiRequestsByModel: React.FC = ({ data={chartData} height={usageChartHeight} loading={query.loading} - colorOffset={1} + colorOffset={5} total={total} /> diff --git a/src/pages/dashboard/components/usage-charts/token-usage-by-model.tsx b/src/pages/dashboard/components/usage-charts/token-usage-by-model.tsx index 1263d58c..f855d122 100644 --- a/src/pages/dashboard/components/usage-charts/token-usage-by-model.tsx +++ b/src/pages/dashboard/components/usage-charts/token-usage-by-model.tsx @@ -38,7 +38,7 @@ const TokenUsageByModel: React.FC = ({ data={chartData} height={usageChartHeight} loading={loading} - colorOffset={3} + colorOffset={2} total={total} /> diff --git a/src/pages/usage/components/daily-usage.tsx b/src/pages/usage/components/daily-usage.tsx index 52d001c4..84dad7b6 100644 --- a/src/pages/usage/components/daily-usage.tsx +++ b/src/pages/usage/components/daily-usage.tsx @@ -1,4 +1,5 @@ -import BarChart, { generateCoolColors } from '@/pages/_components/bar-chart'; +import useCoolColors from '@/hooks/use-cool-colors'; +import BarChart from '@/pages/_components/bar-chart'; import { BaseSelect, CardWrapper } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Segmented } from 'antd'; @@ -78,6 +79,7 @@ const DailyUsage: React.FC = (props) => { onGroupByChange, onGranularityChange } = props; + const generateCoolColors = useCoolColors(); const labelFormatter = (v: any) => { if (granularity === 'month') { @@ -237,7 +239,7 @@ const DailyUsage: React.FC = (props) => { xAxisData: xAxis, legendData: dedupedLegend }; - }, [timeSeriesData, intl]); + }, [timeSeriesData, intl, generateCoolColors]); const handleOnGroupByChange = (value: string) => { onGroupByChange(value || null);