Skip to content

Commit f7608ae

Browse files
committed
fix(skills): align trend chart colors with API keys style
Update the Skills mixed chart so the requests column uses the same purple gradient and corner radius as API Keys, while keeping token lines visually distinct. Also synchronize legend and tooltip labels/colors with the plotted series for faster metric recognition.
1 parent 947abb3 commit f7608ae

File tree

2 files changed

+244
-41
lines changed

2 files changed

+244
-41
lines changed

frontend/src/components/SkillsPanel.jsx

Lines changed: 111 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useMemo, useState } from 'react'
2-
import { AreaChart, Area, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
2+
import { AreaChart, Area, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, ComposedChart, Bar, Line } from 'recharts'
33
import { supabase } from '../lib/supabase'
44
import { CHART_COLORS, CHART_TYPOGRAPHY } from '../lib/brandColors'
55
import ChartDialog from './ChartDialog'
@@ -28,6 +28,13 @@ const formatTprAxis = (value) => {
2828
return Math.round(value).toLocaleString('en-US')
2929
}
3030

31+
const SERIES_COLORS = {
32+
requests: '#8b5cf6',
33+
input: 'var(--color-info)',
34+
output: 'var(--color-cyan)',
35+
cost: 'var(--color-success)',
36+
}
37+
3138
const getStatus = (status) => status === 'failure' ? 'failure' : 'success'
3239
const getSkillLabel = (value) => value || 'Unknown'
3340
const getProjectLabel = (value) => value || 'Unknown Project'
@@ -363,8 +370,6 @@ function SkillsPanel({ skillRuns = [], skillDailyStats = [], dateRange, customRa
363370
}, [activeSkillName, baseRuns])
364371

365372
const trendSeries = trendTime === 'hour' ? overviewHourlySeries : overviewDailySeries
366-
const hasTokenSignal = trendSeries.some(p => (p.input_tokens || 0) > 0 || (p.output_tokens || 0) > 0)
367-
const useRunFallbackSeries = trendSeries.length > 0 && !hasTokenSignal
368373

369374
const detailRuns = useMemo(() => {
370375
if (!activeSkillName) return []
@@ -638,6 +643,13 @@ function SkillsPanel({ skillRuns = [], skillDailyStats = [], dateRange, customRa
638643
{ key: 'top_skill', label: 'Top Skill' },
639644
]
640645

646+
const runAxisTickFormatter = (v) => formatNumber(v)
647+
const tokenAxisTickFormatter = (v) => {
648+
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`
649+
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`
650+
return formatNumber(v)
651+
}
652+
641653
return (
642654
<div className="skills-panel">
643655
<div className="stats-grid">
@@ -719,47 +731,105 @@ function SkillsPanel({ skillRuns = [], skillDailyStats = [], dateRange, customRa
719731
<div className="charts-row">
720732
<div className="chart-card chart-full">
721733
<div className="chart-header">
722-
<h3>Skill Funnel & Token Usage Over Time</h3>
734+
<h3>Skill Requests + Token Trend</h3>
723735
</div>
724736
<div className="chart-body chart-body-dark">
725737
{trendSeries.length > 0 ? (
726-
<ResponsiveContainer width="100%" height={280}>
727-
<AreaChart data={trendSeries} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
728-
<defs>
729-
<linearGradient id="gradSkillTokens" x1="0" y1="0" x2="0" y2="1">
730-
<stop offset="0%" stopColor="#3b82f6" stopOpacity={0.4} />
731-
<stop offset="100%" stopColor="#3b82f6" stopOpacity={0} />
732-
</linearGradient>
733-
</defs>
734-
<CartesianGrid strokeDasharray="4 4" stroke={isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'} />
735-
<XAxis dataKey="label" stroke={isDarkMode ? '#6e7681' : '#57606a'} tick={CHART_TYPOGRAPHY.axisTick} axisLine={false} tickLine={false} />
736-
<YAxis stroke={isDarkMode ? '#6e7681' : '#57606a'} tick={CHART_TYPOGRAPHY.axisTick} axisLine={false} tickLine={false} tickFormatter={(v) => v >= 1_000_000 ? `${(v / 1_000_000).toFixed(1)}M` : v.toLocaleString()} />
737-
<Tooltip content={({ active, payload, label }) => {
738-
if (!active || !payload?.length) return null
739-
const item = payload[0].payload
740-
return (
741-
<div style={{ padding: '8px 10px', background: isDarkMode ? 'rgba(15,23,42,0.95)' : 'white', border: `1px solid ${isDarkMode ? 'rgba(255,255,255,0.08)' : '#e2e8f0'}`, borderRadius: 8 }}>
742-
<div style={{ ...CHART_TYPOGRAPHY.tooltipLabel, marginBottom: 4 }}>{label}</div>
743-
<div style={CHART_TYPOGRAPHY.tooltipItem}>Attempts: {(item.run_count || 0).toLocaleString()}</div>
744-
<div style={CHART_TYPOGRAPHY.tooltipItem}>Success: {(item.success_count || 0).toLocaleString()}</div>
745-
<div style={CHART_TYPOGRAPHY.tooltipItem}>Failure: {(item.failure_count || 0).toLocaleString()}</div>
746-
<div style={CHART_TYPOGRAPHY.tooltipItem}>Input: {formatNumber(item.input_tokens)}</div>
747-
<div style={CHART_TYPOGRAPHY.tooltipItem}>Output: {formatNumber(item.output_tokens)}</div>
748-
{useRunFallbackSeries && <div style={CHART_TYPOGRAPHY.tooltipItem}>Runs: {formatNumber(item.run_count)}</div>}
749-
<div style={{ ...CHART_TYPOGRAPHY.tooltipItem, color: '#10b981' }}>Cost: {formatCost(item.estimated_cost)}</div>
750-
</div>
751-
)
752-
}} />
753-
{useRunFallbackSeries ? (
754-
<Area type="monotone" dataKey="run_count" name="Runs" stroke="#f59e0b" fillOpacity={0.25} fill="#f59e0b" strokeWidth={2} />
755-
) : (
756-
<>
757-
<Area type="monotone" dataKey="input_tokens" name="Input" stroke="#3b82f6" fill="url(#gradSkillTokens)" strokeWidth={2} />
758-
<Area type="monotone" dataKey="output_tokens" name="Output" stroke="#8b5cf6" fillOpacity={0.2} fill="#8b5cf6" strokeWidth={2} />
759-
</>
760-
)}
761-
</AreaChart>
762-
</ResponsiveContainer>
738+
<>
739+
<div className="skills-mixed-chart-legend">
740+
<span className="skills-legend-chip requests">
741+
<span className="skills-legend-dot requests" aria-hidden="true" />
742+
Requests (column)
743+
</span>
744+
<span className="skills-legend-chip input">
745+
<span className="skills-legend-dot input" aria-hidden="true" />
746+
Input Tokens (line)
747+
</span>
748+
<span className="skills-legend-chip output">
749+
<span className="skills-legend-dot output" aria-hidden="true" />
750+
Output Tokens (line)
751+
</span>
752+
</div>
753+
<ResponsiveContainer width="100%" height={280}>
754+
<ComposedChart data={trendSeries} margin={{ top: 10, right: 18, left: 4, bottom: 0 }}>
755+
<defs>
756+
<linearGradient id="gradSkillRequests" x1="0" y1="0" x2="1" y2="0">
757+
<stop offset="0%" stopColor="#8b5cf6" stopOpacity={0.3} />
758+
<stop offset="100%" stopColor="#8b5cf6" stopOpacity={0.9} />
759+
</linearGradient>
760+
</defs>
761+
<CartesianGrid strokeDasharray="3 3" stroke={isDarkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'} vertical={false} />
762+
<XAxis dataKey="label" stroke={isDarkMode ? '#6e7681' : '#57606a'} tick={CHART_TYPOGRAPHY.axisTick} axisLine={false} tickLine={false} />
763+
<YAxis
764+
yAxisId="left"
765+
stroke={isDarkMode ? '#6e7681' : '#57606a'}
766+
tick={CHART_TYPOGRAPHY.axisTick}
767+
axisLine={false}
768+
tickLine={false}
769+
tickFormatter={runAxisTickFormatter}
770+
width={52}
771+
/>
772+
<YAxis
773+
yAxisId="right"
774+
orientation="right"
775+
stroke={isDarkMode ? '#6e7681' : '#57606a'}
776+
tick={CHART_TYPOGRAPHY.axisTick}
777+
axisLine={false}
778+
tickLine={false}
779+
tickFormatter={tokenAxisTickFormatter}
780+
width={56}
781+
/>
782+
<Tooltip
783+
cursor={{ fill: isDarkMode ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)' }}
784+
content={({ active, payload, label }) => {
785+
if (!active || !payload?.length) return null
786+
const item = payload[0].payload
787+
return (
788+
<div className="skills-chart-tooltip">
789+
<div className="skills-chart-tooltip-label">{label}</div>
790+
<div className="skills-chart-tooltip-item requests">Requests: {formatNumber(item.run_count || 0)}</div>
791+
<div className="skills-chart-tooltip-item input">Input Tokens: {formatNumber(item.input_tokens || 0)}</div>
792+
<div className="skills-chart-tooltip-item output">Output Tokens: {formatNumber(item.output_tokens || 0)}</div>
793+
<div className="skills-chart-tooltip-item">Success: {(item.success_count || 0).toLocaleString()}</div>
794+
<div className="skills-chart-tooltip-item">Failure: {(item.failure_count || 0).toLocaleString()}</div>
795+
<div className="skills-chart-tooltip-item cost">Cost: {formatCost(item.estimated_cost)}</div>
796+
</div>
797+
)
798+
}}
799+
/>
800+
<Bar
801+
yAxisId="left"
802+
dataKey="run_count"
803+
name="Requests"
804+
fill="url(#gradSkillRequests)"
805+
stroke={SERIES_COLORS.requests}
806+
strokeWidth={1}
807+
radius={[0, 4, 4, 0]}
808+
maxBarSize={30}
809+
/>
810+
<Line
811+
yAxisId="right"
812+
type="monotone"
813+
dataKey="input_tokens"
814+
name="Input"
815+
stroke={SERIES_COLORS.input}
816+
strokeWidth={2.4}
817+
dot={false}
818+
activeDot={{ r: 4, strokeWidth: 2, fill: isDarkMode ? '#0b1220' : '#ffffff' }}
819+
/>
820+
<Line
821+
yAxisId="right"
822+
type="monotone"
823+
dataKey="output_tokens"
824+
name="Output"
825+
stroke={SERIES_COLORS.output}
826+
strokeWidth={2.4}
827+
dot={false}
828+
activeDot={{ r: 4, strokeWidth: 2, fill: isDarkMode ? '#0b1220' : '#ffffff' }}
829+
/>
830+
</ComposedChart>
831+
</ResponsiveContainer>
832+
</>
763833
) : (
764834
<div className="empty-state">No {trendTime}ly stats yet</div>
765835
)}

frontend/src/styles/index.css

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4251,6 +4251,116 @@ body {
42514251
transition: width 0.4s ease;
42524252
}
42534253

4254+
/* ===== Skills Mixed Chart ===== */
4255+
.skills-mixed-chart-legend {
4256+
display: flex;
4257+
gap: 8px;
4258+
flex-wrap: wrap;
4259+
margin-bottom: 8px;
4260+
padding: 0 4px;
4261+
}
4262+
4263+
.skills-legend-chip {
4264+
display: inline-flex;
4265+
align-items: center;
4266+
gap: 6px;
4267+
font-family: var(--font-body);
4268+
font-size: 11px;
4269+
font-weight: 500;
4270+
color: var(--color-text-secondary);
4271+
border: 1px solid var(--color-border-glass);
4272+
background: var(--color-bg-hover);
4273+
border-radius: var(--radius-sm);
4274+
padding: 3px 8px;
4275+
line-height: 1.3;
4276+
}
4277+
4278+
.skills-legend-dot {
4279+
width: 8px;
4280+
height: 8px;
4281+
border-radius: 999px;
4282+
flex-shrink: 0;
4283+
}
4284+
4285+
.skills-legend-dot.requests {
4286+
background: #8b5cf6;
4287+
box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.28);
4288+
}
4289+
4290+
.skills-legend-dot.input {
4291+
background: var(--color-info);
4292+
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.28);
4293+
}
4294+
4295+
.skills-legend-dot.output {
4296+
background: var(--color-cyan);
4297+
box-shadow: 0 0 0 1px rgba(6, 182, 212, 0.28);
4298+
}
4299+
4300+
.skills-legend-chip.requests {
4301+
border-color: color-mix(in srgb, #8b5cf6 50%, transparent);
4302+
color: #8b5cf6;
4303+
}
4304+
4305+
.skills-legend-chip.input {
4306+
border-color: color-mix(in srgb, var(--color-info) 50%, transparent);
4307+
color: var(--color-info);
4308+
}
4309+
4310+
.skills-legend-chip.output {
4311+
border-color: color-mix(in srgb, var(--color-cyan) 50%, transparent);
4312+
color: var(--color-cyan);
4313+
}
4314+
4315+
.skills-chart-tooltip {
4316+
padding: 8px 10px;
4317+
border-radius: 8px;
4318+
border: 1px solid var(--color-border-glass);
4319+
background: color-mix(in srgb, var(--color-bg-surface) 92%, transparent);
4320+
backdrop-filter: blur(8px);
4321+
-webkit-backdrop-filter: blur(8px);
4322+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
4323+
min-width: 172px;
4324+
}
4325+
4326+
.skills-chart-tooltip-label {
4327+
margin-bottom: 4px;
4328+
font-family: var(--font-heading);
4329+
font-size: 13px;
4330+
font-weight: 600;
4331+
color: var(--color-text);
4332+
}
4333+
4334+
.skills-chart-tooltip-item {
4335+
font-family: var(--font-body);
4336+
font-size: 12px;
4337+
color: var(--color-text-secondary);
4338+
}
4339+
4340+
.skills-chart-tooltip-item + .skills-chart-tooltip-item {
4341+
margin-top: 2px;
4342+
}
4343+
4344+
.skills-chart-tooltip-item.requests {
4345+
color: #8b5cf6;
4346+
}
4347+
4348+
.skills-chart-tooltip-item.input {
4349+
color: var(--color-info);
4350+
}
4351+
4352+
.skills-chart-tooltip-item.output {
4353+
color: var(--color-cyan);
4354+
}
4355+
4356+
.skills-chart-tooltip-item.cost {
4357+
color: var(--color-success);
4358+
}
4359+
4360+
.dashboard.light .skills-chart-tooltip {
4361+
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
4362+
}
4363+
42544364
@media (max-width: 640px) {
42554365
.skill-rank-top {
42564366
flex-direction: column;
@@ -4259,4 +4369,27 @@ body {
42594369
.skill-rank-stats {
42604370
flex-wrap: wrap;
42614371
}
4372+
4373+
.skills-mixed-chart-legend {
4374+
gap: 6px;
4375+
}
4376+
4377+
.skills-legend-chip {
4378+
font-size: 10px;
4379+
padding: 2px 7px;
4380+
}
4381+
}
4382+
4383+
@supports not (color-mix(in srgb, #000 50%, transparent)) {
4384+
.skills-legend-chip.requests {
4385+
border-color: rgba(139, 92, 246, 0.5);
4386+
}
4387+
4388+
.skills-legend-chip.input {
4389+
border-color: rgba(59, 130, 246, 0.5);
4390+
}
4391+
4392+
.skills-legend-chip.output {
4393+
border-color: rgba(6, 182, 212, 0.5);
4394+
}
42624395
}

0 commit comments

Comments
 (0)