Skip to content

Commit ae6ac0c

Browse files
committed
Make cost views monthly-first
1 parent 9fa8ad2 commit ae6ac0c

7 files changed

Lines changed: 482 additions & 264 deletions

File tree

web/src/components/cost/ApplicationCostTab.tsx

Lines changed: 139 additions & 40 deletions
Large diffs are not rendered by default.

web/src/components/cost/CostTrendChart.tsx

Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { useState, useMemo, useRef, useCallback } from 'react'
22
import { clsx } from 'clsx'
33
import { 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

117
const 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

364390
function 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

373394
function formatTimestamp(unix: number): string {

0 commit comments

Comments
 (0)