11import { useState , useMemo , useRef , useCallback } from 'react'
22import { clsx } from 'clsx'
33import { Loader2 , TrendingUp } from 'lucide-react'
4- import {
5- useOpenCostTrend ,
6- type CostTimeRange ,
7- type OpenCostTrendSeries ,
8- } from '../../api/client'
9- import { formatCostAxis } from './format'
4+ import { useOpenCostTrend , type CostTimeRange , type OpenCostTrendSeries } from '../../api/client'
5+ import { formatCostAxis , formatCostPerHour } from './format'
106
117const SERIES_COLORS = [
128 '#3b82f6' , // blue-500
@@ -50,18 +46,21 @@ export function CostTrendChart() {
5046 < div className = "flex items-center justify-between px-4 py-2.5 border-b border-theme-border" >
5147 < div className = "flex items-center gap-2" >
5248 < TrendingUp className = "w-4 h-4 text-theme-text-tertiary" />
53- < span className = "text-xs font-medium text-theme-text-secondary" > Cost Trend</ span >
49+ < div >
50+ < div className = "text-xs font-medium text-theme-text-secondary" > Cost rate trend</ div >
51+ < div className = "text-[10px] text-theme-text-tertiary" > Historical OpenCost hourly allocation</ div >
52+ </ div >
5453 </ div >
5554 < div className = "flex items-center gap-1" >
56- { TIME_RANGES . map ( tr => (
55+ { TIME_RANGES . map ( ( tr ) => (
5756 < button
5857 key = { tr . value }
5958 onClick = { ( ) => setTimeRange ( tr . value ) }
6059 className = { clsx (
6160 'px-2 py-1 text-xs rounded-md transition-colors' ,
6261 timeRange === tr . value
63- ? 'bg-skyhook-600/20 text-blue-400 font-medium'
64- : 'text-theme-text-quaternary hover:text-theme-text-tertiary'
62+ ? 'bg-accent-muted text-accent-text font-medium'
63+ : 'text-theme-text-quaternary hover:text-theme-text-tertiary' ,
6564 ) }
6665 >
6766 { tr . label }
@@ -107,7 +106,7 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
107106 const minTs = timestamps [ 0 ]
108107 const maxTs = timestamps [ timestamps . length - 1 ]
109108
110- const seriesLookups = series . map ( s => {
109+ const seriesLookups = series . map ( ( s ) => {
111110 const map = new Map < number , number > ( )
112111 for ( const dp of s . dataPoints ) {
113112 map . set ( dp . timestamp , dp . value )
@@ -151,20 +150,46 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
151150
152151 // Build stacked area paths
153152 const paths = series . map ( ( _ , si ) => {
154- const topPoints = timestamps . map ( ( ts , ti ) => ( { x : toX ( ts ) , y : toY ( stacked [ si ] [ ti ] ) } ) )
155- const bottomPoints = si > 0
156- ? timestamps . map ( ( ts , ti ) => ( { x : toX ( ts ) , y : toY ( stacked [ si - 1 ] [ ti ] ) } ) )
157- : timestamps . map ( ts => ( { x : toX ( ts ) , y : toY ( 0 ) } ) )
153+ const topPoints = timestamps . map ( ( ts , ti ) => ( {
154+ x : toX ( ts ) ,
155+ y : toY ( stacked [ si ] [ ti ] ) ,
156+ } ) )
157+ const bottomPoints =
158+ si > 0
159+ ? timestamps . map ( ( ts , ti ) => ( {
160+ x : toX ( ts ) ,
161+ y : toY ( stacked [ si - 1 ] [ ti ] ) ,
162+ } ) )
163+ : timestamps . map ( ( ts ) => ( { x : toX ( ts ) , y : toY ( 0 ) } ) )
158164
159165 const topPath = topPoints . map ( ( p , i ) => `${ i === 0 ? 'M' : 'L' } ${ p . x } ,${ p . y } ` ) . join ( ' ' )
160- const bottomPath = [ ...bottomPoints ] . reverse ( ) . map ( ( p , i ) => `${ i === 0 ? 'L' : 'L' } ${ p . x } ,${ p . y } ` ) . join ( ' ' )
166+ const bottomPath = [ ...bottomPoints ]
167+ . reverse ( )
168+ . map ( ( p , i ) => `${ i === 0 ? 'L' : 'L' } ${ p . x } ,${ p . y } ` )
169+ . join ( ' ' )
161170 const areaPath = topPath + ' ' + bottomPath + ' Z'
162171 const linePath = topPoints . map ( ( p , i ) => `${ i === 0 ? 'M' : 'L' } ${ p . x } ,${ p . y } ` ) . join ( ' ' )
163172
164- return { areaPath, linePath, color : SERIES_COLORS [ si % SERIES_COLORS . length ] }
173+ return {
174+ areaPath,
175+ linePath,
176+ color : SERIES_COLORS [ si % SERIES_COLORS . length ] ,
177+ }
165178 } )
166179
167- return { timestamps, stacked, minTs, maxTs, yMax, seriesLookups, toX, toY, yTicks, xTicks, paths }
180+ return {
181+ timestamps,
182+ stacked,
183+ minTs,
184+ maxTs,
185+ yMax,
186+ seriesLookups,
187+ toX,
188+ toY,
189+ yTicks,
190+ xTicks,
191+ paths,
192+ }
168193 } , [ series , plotHeight , plotWidth ] )
169194
170195 // Hover data — depends on hoverX + chartData, must be a separate hook (called unconditionally)
@@ -190,7 +215,11 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
190215 const points = series . map ( ( s , si ) => {
191216 const val = seriesLookups [ si ] . get ( closestTs ) ?? 0
192217 total += val
193- return { namespace : s . namespace , value : val , color : SERIES_COLORS [ si % SERIES_COLORS . length ] }
218+ return {
219+ namespace : s . namespace ,
220+ value : val ,
221+ color : SERIES_COLORS [ si % SERIES_COLORS . length ] ,
222+ }
194223 } )
195224
196225 return { ts : closestTs , x : toX ( closestTs ) , total, points }
@@ -211,18 +240,15 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
211240
212241 return (
213242 < div className = "relative" >
214- < svg
215- ref = { svgRef }
216- viewBox = { `0 0 ${ width } ${ height } ` }
217- className = "w-full"
218- preserveAspectRatio = "xMidYMid meet"
219- >
243+ < svg ref = { svgRef } viewBox = { `0 0 ${ width } ${ height } ` } className = "w-full" preserveAspectRatio = "xMidYMid meet" >
220244 { /* Grid lines */ }
221245 { yTicks . map ( ( tick , i ) => (
222246 < line
223247 key = { `grid-${ i } ` }
224- x1 = { marginLeft } y1 = { tick . y }
225- x2 = { width - marginRight } y2 = { tick . y }
248+ x1 = { marginLeft }
249+ y1 = { tick . y }
250+ x2 = { width - marginRight }
251+ y2 = { tick . y }
226252 stroke = "currentColor"
227253 className = "text-theme-border/30"
228254 strokeWidth = "1"
@@ -262,11 +288,7 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
262288
263289 { /* Stacked area fills (render bottom to top) */ }
264290 { paths . map ( ( p , i ) => (
265- < path
266- key = { `area-${ i } ` }
267- d = { p . areaPath }
268- fill = { p . color + '33' }
269- />
291+ < path key = { `area-${ i } ` } d = { p . areaPath } fill = { p . color + '33' } />
270292 ) ) }
271293
272294 { /* Lines (top edges of each area) */ }
@@ -284,8 +306,10 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
284306 { /* Hover crosshair */ }
285307 { hoverData && (
286308 < line
287- x1 = { hoverData . x } y1 = { marginTop }
288- x2 = { hoverData . x } y2 = { marginTop + plotHeight }
309+ x1 = { hoverData . x }
310+ y1 = { marginTop }
311+ x2 = { hoverData . x }
312+ y2 = { marginTop + plotHeight }
289313 stroke = "currentColor"
290314 className = "text-theme-text-tertiary"
291315 strokeWidth = "1"
@@ -295,8 +319,10 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
295319
296320 { /* Mouse event overlay */ }
297321 < rect
298- x = { marginLeft } y = { marginTop }
299- width = { plotWidth } height = { plotHeight }
322+ x = { marginLeft }
323+ y = { marginTop }
324+ width = { plotWidth }
325+ height = { plotHeight }
300326 fill = "transparent"
301327 style = { { cursor : 'crosshair' } }
302328 onMouseMove = { handleMouseMove }
@@ -316,18 +342,18 @@ export function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] })
316342 < div className = "bg-theme-surface border border-theme-border rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap" >
317343 < div className = "text-theme-text-tertiary mb-1.5 font-mono" >
318344 { new Date ( hoverData . ts * 1000 ) . toLocaleString ( [ ] , {
319- month : 'short' , day : 'numeric' , hour : '2-digit' , minute : '2-digit' ,
345+ month : 'short' ,
346+ day : 'numeric' ,
347+ hour : '2-digit' ,
348+ minute : '2-digit' ,
320349 } ) }
321350 </ div >
322351 { hoverData . points
323- . filter ( p => p . value > 0 )
352+ . filter ( ( p ) => p . value > 0 )
324353 . sort ( ( a , b ) => b . value - a . value )
325354 . map ( ( p , i ) => (
326355 < div key = { i } className = "flex items-center gap-2 py-0.5" >
327- < div
328- className = "w-2 h-2 rounded-full shrink-0"
329- style = { { backgroundColor : p . color } }
330- />
356+ < div className = "w-2 h-2 rounded-full shrink-0" style = { { backgroundColor : p . color } } />
331357 < span className = "text-theme-text-secondary" > { p . namespace } </ span >
332358 < span className = "text-theme-text-primary font-semibold ml-auto pl-3 tabular-nums" >
333359 { formatCostTooltip ( p . value ) }
@@ -362,12 +388,7 @@ export function ChartLegend({ series }: { series: OpenCostTrendSeries[] }) {
362388}
363389
364390function formatCostTooltip ( value : number ) : string {
365- if ( value >= 1000 ) return `$${ ( value / 1000 ) . toFixed ( 1 ) } k/hr`
366- if ( value >= 1 ) return `$${ value . toFixed ( 2 ) } /hr`
367- if ( value >= 0.01 ) return `$${ value . toFixed ( 3 ) } /hr`
368- if ( value > 0 && value < 0.0001 ) return `${ formatCostAxis ( value ) } /hr`
369- if ( value > 0 ) return `$${ value . toFixed ( 4 ) } /hr`
370- return '$0.00/hr'
391+ return formatCostPerHour ( value )
371392}
372393
373394function formatTimestamp ( unix : number ) : string {
0 commit comments