[]
+ xAxisKey?: string
isPercentage?: boolean
format?: string | ((value: unknown) => string)
valuePrecision?: number
@@ -139,6 +145,8 @@ export const CustomTooltip = ({
payload,
label,
attributes,
+ data,
+ xAxisKey = 'period_start',
isPercentage,
format,
valuePrecision,
@@ -152,10 +160,15 @@ export const CustomTooltip = ({
const firstItem = payload[0].payload
const timestampKey = firstItem?.hasOwnProperty('timestamp') ? 'timestamp' : 'period_start'
const timestamp = payload[0].payload[timestampKey]
+ const rawDataPoint = data?.find(
+ (point) => point[xAxisKey] === timestamp || point[timestampKey] === timestamp
+ )
const maxValueAttribute = isMaxAttribute(attributes)
- const maxValueData =
- maxValueAttribute && payload?.find((p: any) => p.dataKey === maxValueAttribute.attribute)
- const maxValue = maxValueData?.value
+ const maxValue =
+ maxValueAttribute && rawDataPoint
+ ? Number(rawDataPoint[maxValueAttribute.attribute])
+ : undefined
+ const hasFiniteMaxValue = typeof maxValue === 'number' && Number.isFinite(maxValue)
const isRamChart =
!payload?.some((p: any) => p.dataKey.toLowerCase() === 'ram_usage') &&
payload?.some((p: any) => p.dataKey.toLowerCase().includes('ram_'))
@@ -182,7 +195,15 @@ export const CustomTooltip = ({
const localTimeZone = dayjs.tz.guess()
- const total = showTotal && calculateTotalChartAggregate(payload, attributesToIgnoreFromTotal)
+ const rawPayload = payload.map((entry: any) => ({
+ ...entry,
+ value:
+ rawDataPoint && typeof rawDataPoint[entry.dataKey] === 'number'
+ ? Number(rawDataPoint[entry.dataKey])
+ : entry.value,
+ }))
+
+ const total = showTotal && calculateTotalChartAggregate(rawPayload, attributesToIgnoreFromTotal)
const getIcon = (color: string, isMax: boolean) =>
isMax ? :
@@ -196,7 +217,14 @@ export const CustomTooltip = ({
const LabelItem = ({ entry }: { entry: any }) => {
const attribute = attributes?.find((a: MultiAttribute) => a?.attribute === entry.name)
- const percentage = ((entry.value / maxValue) * 100).toFixed(valuePrecision)
+ const rawValue =
+ rawDataPoint && typeof rawDataPoint[entry.dataKey] === 'number'
+ ? Number(rawDataPoint[entry.dataKey])
+ : entry.value
+ const percentage =
+ hasFiniteMaxValue && maxValue > 0
+ ? ((rawValue / maxValue) * 100).toFixed(valuePrecision)
+ : null
const isMax = entry.dataKey === maxValueAttribute?.attribute
return (
@@ -206,12 +234,12 @@ export const CustomTooltip = ({
{attribute?.label || entry.name}
- {formatNumeric(entry.value) + (!isPercentage && format !== 'ms' ? byteUnitSuffix : '')}
+ {formatNumeric(rawValue) + (!isPercentage && format !== 'ms' ? byteUnitSuffix : '')}
{isPercentage ? '%' : ''}
{format === 'ms' ? 'ms' : ''}
{/* Show percentage if max value is set */}
- {!!maxValueData && !isMax && !isPercentage && (
+ {percentage !== null && !isMax && !isPercentage && (
({percentage}%)
)}
@@ -229,7 +257,7 @@ export const CustomTooltip = ({
{localTimeZone}
{dayjs(timestamp).format(DateTimeFormats.FULL_SECONDS)}
- {payload.reverse().map((entry: any, index: number) => (
+ {[...payload].reverse().map((entry: any, index: number) => (
))}
{active && showTotal && (
@@ -244,11 +272,12 @@ export const CustomTooltip = ({
{format === 'ms' ? 'ms' : ''}
{maxValueAttribute &&
+ hasFiniteMaxValue &&
!isPercentage &&
- !isNaN((total as number) / maxValueData?.value) &&
- isFinite((total as number) / maxValueData?.value) && (
+ !isNaN((total as number) / maxValue) &&
+ isFinite((total as number) / maxValue) && (
- ({(((total as number) / maxValueData?.value) * 100).toFixed(1)}%)
+ ({(((total as number) / maxValue) * 100).toFixed(1)}%)
)}
diff --git a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx
index 85799e033e6..8f6efde584d 100644
--- a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx
+++ b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx
@@ -1,24 +1,22 @@
-import { List, Loader2 } from 'lucide-react'
-import { useRouter } from 'next/router'
-import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'
-import { Card, cn, WarningIcon } from 'ui'
-
import Panel from 'components/ui/Panel'
-import type { ChartHighlightAction } from './ChartHighlightActions'
-import { ComposedChart } from './ComposedChart'
-
import { AnalyticsInterval, DataPoint } from 'data/analytics/constants'
import { useInfraMonitoringQueries } from 'data/analytics/infra-monitoring-queries'
import { InfraMonitoringAttribute } from 'data/analytics/infra-monitoring-query'
import { useProjectDailyStatsQueries } from 'data/analytics/project-daily-stats-queries'
import { ProjectDailyStatsAttribute } from 'data/analytics/project-daily-stats-query'
-import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
-import { useChartHighlight } from './useChartHighlight'
-
import dayjs from 'dayjs'
+import { List, Loader2 } from 'lucide-react'
+import { useRouter } from 'next/router'
import type { UpdateDateRange } from 'pages/project/[ref]/observability/database'
+import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'
+import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
+import { Card, cn, WarningIcon } from 'ui'
+
+import type { ChartHighlightAction } from './ChartHighlightActions'
import type { ChartData } from './Charts.types'
+import { ComposedChart } from './ComposedChart'
import { MultiAttribute } from './ComposedChart.utils'
+import { useChartHighlight } from './useChartHighlight'
export interface ComposedChartHandlerProps {
id?: string
@@ -30,7 +28,7 @@ export interface ComposedChartHandlerProps {
customDateFormat?: string
defaultChartStyle?: 'bar' | 'line' | 'stackedAreaLine'
hideChartType?: boolean
- data?: ChartData
+ data?: ChartData | DataPoint[]
isLoading?: boolean
format?: string
highlightedValue?: string | number
@@ -39,6 +37,7 @@ export interface ComposedChartHandlerProps {
showLegend?: boolean
showTotal?: boolean
showMaxValue?: boolean
+ normalizeVisibleStackToPercent?: boolean
updateDateRange?: UpdateDateRange
valuePrecision?: number
isVisible?: boolean
@@ -134,12 +133,12 @@ const ComposedChartHandler = ({
endDate,
interval as AnalyticsInterval,
databaseIdentifier,
- data,
+ Array.isArray(data) ? undefined : data,
isVisible
)
const combinedData = useMemo(() => {
- if (data) return data
+ if (data) return Array.isArray(data) ? data : data.data
const isLoading = attributeQueries.some((query: any) => query.isLoading)
if (isLoading) return undefined
diff --git a/apps/studio/data/reports/database-charts.ts b/apps/studio/data/reports/database-charts.ts
index d37aea3a05b..8c3cdbea739 100644
--- a/apps/studio/data/reports/database-charts.ts
+++ b/apps/studio/data/reports/database-charts.ts
@@ -1,4 +1,4 @@
-import { compactNumberFormatter, numberFormatter } from 'components/ui/Charts/Charts.utils'
+import { compactNumberFormatter } from 'components/ui/Charts/Charts.utils'
import { ReportAttributes } from 'components/ui/Charts/ComposedChart.utils'
import { DOCS_URL } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
@@ -76,13 +76,12 @@ export const getReportAttributesV2: (
showLegend: true,
showMaxValue: false,
showGrid: true,
+ normalizeVisibleStackToPercent: true,
YAxisProps: {
- width: 45,
- tickFormatter: (value: any) => {
- // avoid displaying 100.00%
- if (value === 100) return '100%'
- return `${numberFormatter(value, 2)}%`
- },
+ width: 55,
+ domain: [0, 100] as [number, number],
+ allowDataOverflow: true,
+ tickFormatter: (v: number) => `${Math.round(v)}%`,
},
hideChartType: false,
defaultChartStyle: 'bar',
@@ -92,6 +91,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'System',
format: '%',
+ color: { light: '#EDC35E', dark: '#EDD35E' },
+ fill: { light: '#F6D99F', dark: '#5C5230' },
tooltip:
'CPU time spent on kernel operations (e.g., process scheduling, memory management). High values may indicate system overhead',
},
@@ -100,6 +101,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'User',
format: '%',
+ color: { light: '#0063E8', dark: '#65BCD9' },
+ fill: { light: '#80B1F4', dark: '#2A3D45' },
tooltip:
'CPU time used by database queries and user-space processes. High values may suggest CPU-intensive queries',
},
@@ -108,6 +111,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'IOwait',
format: '%',
+ color: { light: '#DB3A34', dark: '#FF6B6B' },
+ fill: { light: '#F2A7A3', dark: '#5C2A2A' },
tooltip:
'CPU time waiting for disk or network I/O. High values may indicate disk bottlenecks',
},
@@ -116,6 +121,8 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'IRQs',
format: '%',
+ color: { light: '#DA760B', dark: '#DA760B' },
+ fill: { light: '#FFB885', dark: '#5C3D0A' },
tooltip: 'CPU time handling hardware interrupt requests (IRQ)',
},
{
@@ -123,9 +130,21 @@ export const getReportAttributesV2: (
provider: 'infra-monitoring',
label: 'Other',
format: '%',
+ color: { light: '#B616A6', dark: '#DB8DF9' },
+ fill: { light: '#DB8BD3', dark: '#4A3D5C' },
tooltip:
'CPU time spent on other tasks (e.g., background processes, software interrupts)',
},
+ {
+ attribute: 'cpu_usage_busy_idle',
+ provider: 'infra-monitoring',
+ label: 'Idle',
+ format: '%',
+ omitFromTotal: true,
+ color: { light: '#6EA85F', dark: '#A3FFC2' },
+ fill: { light: '#A6D8AE', dark: '#2A5C3F' },
+ tooltip: 'CPU time spent idle and available for new work',
+ },
{
attribute: 'cpu_usage_max',
provider: 'reference-line',