diff --git a/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx b/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx index 61a9e249e63..74e2700e2b9 100644 --- a/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx +++ b/apps/studio/components/interfaces/Reports/ReportBlock/ChartBlock.tsx @@ -12,7 +12,9 @@ import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder' import { checkHasNonPositiveValues, + computeYAxisWidth, formatLogTick, + formatYAxisTick, } from '@/components/ui/QueryBlock/QueryBlock.utils' import { AnalyticsInterval } from '@/data/analytics/constants' import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries' @@ -176,6 +178,11 @@ export const ChartBlock = ({ const effectiveLogScale = logScale && !hasNonPositiveValues + const yAxisWidth = computeYAxisWidth(data, metricLabel, { + isLogScale: effectiveLogScale, + isPercentage, + }) + const getInitialHighlightedValue = useCallback(() => { if (!chartData?.data?.length) return undefined const lastDataPoint = chartData.data[chartData.data.length - 1] @@ -301,13 +308,13 @@ export const ChartBlock = ({ scale={effectiveLogScale ? 'log' : 'auto'} domain={effectiveLogScale ? [1, 'auto'] : isPercentage ? [0, 100] : undefined} allowDataOverflow={effectiveLogScale} - width={effectiveLogScale ? 52 : undefined} - tickFormatter={effectiveLogScale ? formatLogTick : undefined} + width={yAxisWidth} + tickFormatter={effectiveLogScale ? formatLogTick : formatYAxisTick} /> dayjs(x).format('DD MMM YYYY')} /> @@ -329,13 +336,12 @@ export const ChartBlock = ({ scale={effectiveLogScale ? 'log' : 'auto'} domain={effectiveLogScale ? [1, 'auto'] : isPercentage ? [0, 100] : undefined} allowDataOverflow={effectiveLogScale} - width={effectiveLogScale ? 52 : undefined} - tickFormatter={effectiveLogScale ? formatLogTick : undefined} + width={yAxisWidth} + tickFormatter={effectiveLogScale ? formatLogTick : formatYAxisTick} /> dayjs(x).format('DD MMM YYYY')} /> diff --git a/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx b/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx index 6cea21b65dc..8952027bed8 100644 --- a/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx +++ b/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx @@ -56,7 +56,7 @@ export const ReportBlockContainer = ({

{label}

{badge &&
{badge}
} -
{actions}
+
{actions}
{tooltip && ( diff --git a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx index f1f9fa09ae2..df6159a24a3 100644 --- a/apps/studio/components/ui/QueryBlock/QueryBlock.tsx +++ b/apps/studio/components/ui/QueryBlock/QueryBlock.tsx @@ -11,7 +11,13 @@ import { CHART_COLORS } from '../Charts/Charts.constants' import { SqlWarningAdmonition } from '../SqlWarningAdmonition' import { BlockViewConfiguration } from './BlockViewConfiguration' import { EditQueryButton } from './EditQueryButton' -import { checkHasNonPositiveValues, formatLogTick, getCumulativeResults } from './QueryBlock.utils' +import { + checkHasNonPositiveValues, + computeYAxisWidth, + formatLogTick, + formatYAxisTick, + getCumulativeResults, +} from './QueryBlock.utils' import { ReportBlockContainer } from '@/components/interfaces/Reports/ReportBlock/ReportBlockContainer' import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig' import Results from '@/components/interfaces/SQLEditor/UtilityPanel/Results' @@ -113,6 +119,10 @@ export const QueryBlock = ({ const effectiveLogScale = logScale && !hasNonPositiveValues + const yAxisWidth = computeYAxisWidth(chartData ?? [], yKey ?? '', { + isLogScale: effectiveLogScale, + }) + const getDateFormat = (key: any) => { const value = chartData?.[0]?.[key] || '' if (typeof value === 'number') return 'number' @@ -226,9 +236,12 @@ export const QueryBlock = ({ {showSql && (
{ if (e.activeTooltipIndex !== focusDataIndex) { @@ -300,11 +313,22 @@ export const QueryBlock = ({ scale={effectiveLogScale ? 'log' : 'auto'} domain={effectiveLogScale ? [1, 'auto'] : undefined} allowDataOverflow={effectiveLogScale} - width={effectiveLogScale ? 52 : undefined} - tickFormatter={effectiveLogScale ? formatLogTick : undefined} + width={yAxisWidth} + tickFormatter={effectiveLogScale ? formatLogTick : formatYAxisTick} /> - } /> - + + xKeyDateFormat === 'date' + ? dayjs(value).format('MMM D YYYY HH:mm') + : String(value) + } + /> + } + /> + {chartData?.map((_: any, index: number) => ( ) : ( results && ( -
+
) diff --git a/apps/studio/components/ui/QueryBlock/QueryBlock.utils.test.ts b/apps/studio/components/ui/QueryBlock/QueryBlock.utils.test.ts new file mode 100644 index 00000000000..8581224afce --- /dev/null +++ b/apps/studio/components/ui/QueryBlock/QueryBlock.utils.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it } from 'vitest' + +import { computeYAxisWidth, formatYAxisTick } from './QueryBlock.utils' + +describe('formatYAxisTick', () => { + it('returns integers as-is when below 1000', () => { + expect(formatYAxisTick(0)).toBe('0') + expect(formatYAxisTick(1)).toBe('1') + expect(formatYAxisTick(999)).toBe('999') + }) + + it('abbreviates thousands with K', () => { + expect(formatYAxisTick(1_000)).toBe('1K') + expect(formatYAxisTick(5_000)).toBe('5K') + expect(formatYAxisTick(999_000)).toBe('999K') + }) + + it('rounds thousands to one decimal place', () => { + expect(formatYAxisTick(1_500)).toBe('1.5K') + expect(formatYAxisTick(55_300)).toBe('55.3K') + expect(formatYAxisTick(1_234)).toBe('1.2K') + }) + + it('abbreviates millions with M', () => { + expect(formatYAxisTick(1_000_000)).toBe('1M') + expect(formatYAxisTick(2_000_000)).toBe('2M') + }) + + it('rounds millions to one decimal place', () => { + expect(formatYAxisTick(1_500_000)).toBe('1.5M') + expect(formatYAxisTick(3_208_914)).toBe('3.2M') + }) + + it('handles values just below the million threshold', () => { + expect(formatYAxisTick(999_900)).toBe('999.9K') + }) + + it('handles negative values', () => { + expect(formatYAxisTick(-1_000)).toBe('-1K') + expect(formatYAxisTick(-1_500)).toBe('-1.5K') + expect(formatYAxisTick(-1_000_000)).toBe('-1M') + expect(formatYAxisTick(-999)).toBe('-999') + }) + + it('rounds small decimals to 2 places', () => { + expect(formatYAxisTick(0.456)).toBe('0.46') + expect(formatYAxisTick(0.1)).toBe('0.1') + expect(formatYAxisTick(-0.123)).toBe('-0.12') + }) + + it('rounds non-integer values between 1 and 1000 to 1 decimal place', () => { + expect(formatYAxisTick(1.25)).toBe('1.3') + expect(formatYAxisTick(99.9)).toBe('99.9') + expect(formatYAxisTick(5.0)).toBe('5') + }) +}) + +describe('computeYAxisWidth', () => { + const row = (v: number) => ({ val: v }) + + it('returns 52 for log scale regardless of data', () => { + expect(computeYAxisWidth([row(1_000_000)], 'val', { isLogScale: true })).toBe(52) + }) + + it('returns a fixed width for percentage data', () => { + const width = computeYAxisWidth([row(99)], 'val', { isPercentage: true }) + // "100" is the longest tick → (3+1)*8 = 32, floor at 36 + expect(width).toBe(36) + }) + + it('returns minimum 36 for small values', () => { + expect(computeYAxisWidth([row(5)], 'val')).toBe(36) + expect(computeYAxisWidth([], 'val')).toBe(36) + }) + + it('widens for large values', () => { + // formatYAxisTick(55_300) = "55.3K" (5 chars) → (5+1)*8 = 48 + expect(computeYAxisWidth([row(55_300)], 'val')).toBe(48) + }) + + it('uses absolute magnitude so negative data is handled correctly', () => { + const negWidth = computeYAxisWidth([row(-55_300)], 'val') + const posWidth = computeYAxisWidth([row(55_300)], 'val') + expect(negWidth).toBe(posWidth) + }) + + it('picks the largest magnitude across all rows', () => { + const data = [row(100), row(5_000), row(200)] + // max is 5000 → "5K" (2 chars) → (2+1)*8 = 24, floor at 36 + expect(computeYAxisWidth(data, 'val')).toBe(36) + }) +}) diff --git a/apps/studio/components/ui/QueryBlock/QueryBlock.utils.ts b/apps/studio/components/ui/QueryBlock/QueryBlock.utils.ts index 10ebd76b9f8..884b6d58663 100644 --- a/apps/studio/components/ui/QueryBlock/QueryBlock.utils.ts +++ b/apps/studio/components/ui/QueryBlock/QueryBlock.utils.ts @@ -3,6 +3,39 @@ import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/Char export const checkHasNonPositiveValues = (data: Record[], key: string): boolean => data.some((row) => (row[key] as number) <= 0) +export const formatYAxisTick = (value: number): string => { + if (Math.abs(value) >= 1_000_000) { + const n = value / 1_000_000 + return `${Number.isInteger(n) ? n : n.toFixed(1)}M` + } + if (Math.abs(value) >= 1_000) { + const n = value / 1_000 + return `${Number.isInteger(n) ? n : n.toFixed(1)}K` + } + if (value !== 0 && Math.abs(value) < 1) { + return parseFloat(value.toFixed(2)).toString() + } + if (!Number.isInteger(value)) { + return parseFloat(value.toFixed(1)).toString() + } + return String(value) +} + +export const computeYAxisWidth = ( + data: Record[], + key: string, + { + isLogScale = false, + isPercentage = false, + }: { isLogScale?: boolean; isPercentage?: boolean } = {} +): number => { + if (isLogScale) return 52 + if (isPercentage) return Math.max(36, (3 + 1) * 8) // max tick is "100" + const maxMagnitude = + data.length > 0 ? Math.max(...data.map((d) => Math.abs(Number(d[key]) || 0))) : 0 + return Math.max(36, (formatYAxisTick(maxMagnitude).length + 1) * 8) +} + export const formatLogTick = (value: number): string => { if (value >= 1_000_000) return `${(value / 1_000_000).toLocaleString(undefined, { maximumFractionDigits: 1 })}M` @@ -11,7 +44,6 @@ export const formatLogTick = (value: number): string => { return value.toLocaleString() } -// Add helper function for cumulative results export const getCumulativeResults = (results: { rows: any[] }, config: ChartConfig) => { if (!results?.rows?.length) { return []