From ccf2bfaee3937c532ee7e7186ff4c09740b065bd Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 12 Jun 2026 10:33:04 -0500 Subject: [PATCH 1/2] fix: add line chart axis labels and hover tooltips --- apps/dashboard/src/routes/index.tsx | 214 ++++++++++++++++++++++++---- apps/dashboard/src/styles.css | 27 ++++ 2 files changed, 210 insertions(+), 31 deletions(-) diff --git a/apps/dashboard/src/routes/index.tsx b/apps/dashboard/src/routes/index.tsx index e0abc19..e38664f 100644 --- a/apps/dashboard/src/routes/index.tsx +++ b/apps/dashboard/src/routes/index.tsx @@ -1173,55 +1173,107 @@ function TrafficBars({ compactLabels = false, data, maxLabels = 6, rotateLabels } function TrafficTrendChart({ data, title }: TrafficTrendChartProps) { - const requestSegments = toPolylineSegments(data.map((item) => ({ missing: item.missing, value: item.primary })), { - fillMissingWithZero: true, - }) - const costSegments = toPolylineSegments(data.map((item) => ({ missing: item.missing, value: item.secondary })), { - fillMissingWithZero: true, - }) - const cacheSegments = toPolylineSegments(data.map((item) => ({ missing: item.missing, value: item.tertiary })), { - fillMissingWithZero: true, - }) + const requestSeries = buildLineChartSeries( + data.map((item) => ({ missing: item.missing, value: item.primary })), + { fillMissingWithZero: true }, + ) + const costSeries = buildLineChartSeries( + data.map((item) => ({ missing: item.missing, value: item.secondary })), + { fillMissingWithZero: true }, + ) + const cacheSeries = buildLineChartSeries( + data.map((item) => ({ missing: item.missing, value: item.tertiary })), + { fillMissingWithZero: true }, + ) + const ticks = buildLineChartTicks(data.map((item) => item.day)) + const hoverTargets = buildLineChartHoverTargets(data.length) return ( {title} - - - - {requestSegments.map((points, index) => ( + + + + {requestSeries.segments.map((points, index) => ( ))} - {costSegments.map((points, index) => ( + {costSeries.segments.map((points, index) => ( ))} - {cacheSegments.map((points, index) => ( + {cacheSeries.segments.map((points, index) => ( ))} + {requestSeries.points.map((point) => ( + + ))} + {costSeries.points.map((point) => ( + + ))} + {cacheSeries.points.map((point) => ( + + ))} + {hoverTargets.map((target, index) => ( + + {formatTrafficTooltip(data[index])} + + + ))} + {ticks.map((tick) => ( + + {formatBucketLabel(tick.day)} + + {formatLineAxisLabel(tick.day)} + + + ))} ) } function LineChart({ data, title }: LineChartProps) { - const primarySegments = toPolylineSegments(data.map((item) => ({ missing: item.missing, value: item.primary })), { - fillMissingWithZero: true, - }) - const secondarySegments = toPolylineSegments(data.map((item) => ({ missing: item.missing, value: item.secondary })), { - fillMissingWithZero: true, - }) + const primarySeries = buildLineChartSeries( + data.map((item) => ({ missing: item.missing, value: item.primary })), + { fillMissingWithZero: true }, + ) + const secondarySeries = buildLineChartSeries( + data.map((item) => ({ missing: item.missing, value: item.secondary })), + { fillMissingWithZero: true }, + ) + const ticks = buildLineChartTicks(data.map((item) => item.day)) + const hoverTargets = buildLineChartHoverTargets(data.length) return ( {title} - - - - {primarySegments.map((points, index) => ( + + + + {primarySeries.segments.map((points, index) => ( ))} - {secondarySegments.map((points, index) => ( + {secondarySeries.segments.map((points, index) => ( ))} + {primarySeries.points.map((point) => ( + + ))} + {secondarySeries.points.map((point) => ( + + ))} + {hoverTargets.map((target, index) => ( + + {formatInputOutputTooltip(data[index])} + + + ))} + {ticks.map((tick) => ( + + {formatBucketLabel(tick.day)} + + {formatLineAxisLabel(tick.day)} + + + ))} ) } @@ -1626,6 +1678,18 @@ function formatDayShort(value: string, compact = false) { }).format(date) } +function formatLineAxisLabel(value: string) { + const isTimestamp = value.includes('T') + const date = isTimestamp ? new Date(value) : new Date(`${value}T00:00:00Z`) + + return new Intl.DateTimeFormat('en-US', { + day: 'numeric', + hour: isTimestamp ? 'numeric' : undefined, + month: 'numeric', + timeZone: isTimestamp ? DASHBOARD_TIME_ZONE : 'UTC', + }).format(date) +} + function shouldRenderTick(index: number, total: number, maxLabels = 6) { if (total <= maxLabels) return true if (index === 0 || index === total - 1) return true @@ -1676,8 +1740,7 @@ function toPolylineSegments( points: Array<{ missing?: boolean; value: number }>, options?: { fillMissingWithZero?: boolean }, ) { - const presentValues = points.filter((point) => !point.missing).map((point) => point.value) - const maxValue = Math.max(...presentValues, 1) + const maxValue = getLineChartMaxValue(points) const segments: string[] = [] let currentSegment: string[] = [] @@ -1690,10 +1753,8 @@ function toPolylineSegments( return } - const x = 16 + index * (288 / Math.max(points.length - 1, 1)) const value = point.missing && options?.fillMissingWithZero ? 0 : point.value - const y = 130 - (value / maxValue) * 112 - currentSegment.push(`${x},${y}`) + currentSegment.push(`${getLineChartX(index, points.length)},${getLineChartY(value, maxValue)}`) }) if (currentSegment.length >= 2) { @@ -1703,6 +1764,97 @@ function toPolylineSegments( return segments } +function buildLineChartSeries( + points: Array<{ missing?: boolean; value: number }>, + options?: { fillMissingWithZero?: boolean }, +) { + const maxValue = getLineChartMaxValue(points) + + return { + points: points.flatMap((point, index) => { + if (point.missing) { + return [] + } + + return [{ + index, + value: point.value, + x: getLineChartX(index, points.length), + y: getLineChartY(point.value, maxValue), + }] + }), + segments: toPolylineSegments(points, options), + } +} + +function buildLineChartTicks(days: string[], maxLabels = 6) { + return days.flatMap((day, index) => { + if (!shouldRenderTick(index, days.length, maxLabels)) { + return [] + } + + return [{ + day, + x: getLineChartX(index, days.length), + }] + }) +} + +function buildLineChartHoverTargets(total: number) { + if (total === 0) { + return [] + } + + const slotWidth = total > 1 ? LINE_CHART_WIDTH / (total - 1) : LINE_CHART_WIDTH + + return Array.from({ length: total }, (_, index) => ({ + width: slotWidth, + x: Math.max(0, Math.min(320 - slotWidth, getLineChartX(index, total) - slotWidth / 2)), + })) +} + +function getLineChartMaxValue(points: Array<{ missing?: boolean; value: number }>) { + const presentValues = points.filter((point) => !point.missing).map((point) => point.value) + return Math.max(...presentValues, 1) +} + +function getLineChartX(index: number, total: number) { + return LINE_CHART_LEFT + index * (LINE_CHART_WIDTH / Math.max(total - 1, 1)) +} + +function getLineChartY(value: number, maxValueOrPoints: number | Array<{ missing?: boolean; value: number }>) { + const maxValue = + typeof maxValueOrPoints === 'number' + ? maxValueOrPoints + : getLineChartMaxValue(maxValueOrPoints) + + return LINE_CHART_BOTTOM - (value / maxValue) * LINE_CHART_HEIGHT +} + +function formatTrafficTooltip(point: TrafficTrendChartProps['data'][number]) { + return [ + formatBucketLabel(point.day), + `Requests: ${point.primary.toLocaleString('en-US')}`, + `Allocated cost: ${formatCurrency(point.secondary / 10)}`, + `Cached share: ${point.tertiary.toFixed(1)}%`, + ].join('\n') +} + +function formatInputOutputTooltip(point: LineChartProps['data'][number]) { + return [ + formatBucketLabel(point.day), + `Input tokens: ${point.primary.toLocaleString('en-US')}`, + `Output tokens: ${point.secondary.toLocaleString('en-US')}`, + ].join('\n') +} + +const LINE_CHART_LEFT = 16 +const LINE_CHART_RIGHT = 304 +const LINE_CHART_TOP = 16 +const LINE_CHART_BOTTOM = 126 +const LINE_CHART_WIDTH = LINE_CHART_RIGHT - LINE_CHART_LEFT +const LINE_CHART_HEIGHT = LINE_CHART_BOTTOM - LINE_CHART_TOP + const toneClassNameMap = { negative: 'text-rose-600', neutral: 'text-slate-500', diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index 8028015..9c81af5 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -693,6 +693,33 @@ stroke: var(--chart-red); } +.chart-point { + fill: var(--chart-ink); + stroke: #fff; + stroke-width: 1.25; +} + +.chart-point-muted { + fill: var(--chart-violet); +} + +.chart-point-grey { + fill: var(--chart-grey); +} + +.chart-point-red { + fill: var(--chart-red); +} + +.chart-hover-target { + fill: transparent; +} + +.chart-axis-label { + fill: #64748b; + font-size: 8px; +} + .panel-card-signals { min-height: 0; } From 3d73537adadecfaaeb2b688b724562bc9b6bff0b Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 12 Jun 2026 11:36:38 -0500 Subject: [PATCH 2/2] fix: tune line chart axis labels --- apps/dashboard/src/routes/index.tsx | 29 +++++++++++++++++++++-------- apps/dashboard/src/styles.css | 11 ++++++----- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/apps/dashboard/src/routes/index.tsx b/apps/dashboard/src/routes/index.tsx index e38664f..84b6d4b 100644 --- a/apps/dashboard/src/routes/index.tsx +++ b/apps/dashboard/src/routes/index.tsx @@ -1189,7 +1189,7 @@ function TrafficTrendChart({ data, title }: TrafficTrendChartProps) { const hoverTargets = buildLineChartHoverTargets(data.length) return ( - + {title} @@ -1221,7 +1221,7 @@ function TrafficTrendChart({ data, title }: TrafficTrendChartProps) { {ticks.map((tick) => ( {formatBucketLabel(tick.day)} - + {formatLineAxisLabel(tick.day)} @@ -1243,7 +1243,7 @@ function LineChart({ data, title }: LineChartProps) { const hoverTargets = buildLineChartHoverTargets(data.length) return ( - + {title} @@ -1269,7 +1269,7 @@ function LineChart({ data, title }: LineChartProps) { {ticks.map((tick) => ( {formatBucketLabel(tick.day)} - + {formatLineAxisLabel(tick.day)} @@ -1682,12 +1682,23 @@ function formatLineAxisLabel(value: string) { const isTimestamp = value.includes('T') const date = isTimestamp ? new Date(value) : new Date(`${value}T00:00:00Z`) + if (isTimestamp) { + return new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + timeZone: DASHBOARD_TIME_ZONE, + }) + .format(date) + .replace(/\s/g, '') + .toLowerCase() + } + return new Intl.DateTimeFormat('en-US', { day: 'numeric', - hour: isTimestamp ? 'numeric' : undefined, - month: 'numeric', - timeZone: isTimestamp ? DASHBOARD_TIME_ZONE : 'UTC', - }).format(date) + month: 'short', + timeZone: 'UTC', + }) + .format(date) + .toLowerCase() } function shouldRenderTick(index: number, total: number, maxLabels = 6) { @@ -1852,6 +1863,8 @@ const LINE_CHART_LEFT = 16 const LINE_CHART_RIGHT = 304 const LINE_CHART_TOP = 16 const LINE_CHART_BOTTOM = 126 +const LINE_CHART_LABEL_Y = 140 +const LINE_CHART_VIEWBOX_HEIGHT = 156 const LINE_CHART_WIDTH = LINE_CHART_RIGHT - LINE_CHART_LEFT const LINE_CHART_HEIGHT = LINE_CHART_BOTTOM - LINE_CHART_TOP diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index 9c81af5..ceb607a 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -601,8 +601,8 @@ width: 100%; max-width: 100%; height: auto; - min-height: 228px; - aspect-ratio: 320 / 150; + min-height: 236px; + aspect-ratio: 320 / 156; } @media (max-width: 768px) { @@ -644,7 +644,7 @@ } .line-chart { - min-height: 188px; + min-height: 196px; } } @@ -716,8 +716,9 @@ } .chart-axis-label { - fill: #64748b; - font-size: 8px; + fill: #334155; + font-size: 9px; + font-weight: 600; } .panel-card-signals {