Files
supabase/apps/studio/components/ui/QueryBlock/QueryBlock.utils.ts
Joshen Lim 6fd48944a8 Support multi series bar charts in explorer and chart-bar (#49241)
## Context

- Updates the BarChart in our design system to support multi series in a
similar fashion to how the LineChart already supports multi series
- Update chart renderer in explorer notebooks to support multiple Y axes
using the `MultiSelector` component
- Up to 3 y columns can be selected for now (Arbitrary limit from a
color's selection POV but also just felt like anything more and the
chart doesn't feel useful)
- Only linear scale will be supported if multiple y columns are selected
(Will switch back to linear if originally on log scale)

<img width="943" height="493" alt="image"
src="https://github.com/user-attachments/assets/2eba46f0-7e41-4544-a3ff-2bf08773d11b"
/>
<img width="946" height="497" alt="image"
src="https://github.com/user-attachments/assets/4ffe7a73-6f97-4f0d-a33a-31e4035800ab"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Charts now support selecting and displaying up to three Y-axis data
series.
  * Bar and line charts render multiple series with distinct colors.
* Cumulative calculations work independently across multiple selected
series.
* Chart controls provide clearer responsive layouts and limit selections
appropriately.

* **Bug Fixes**
* Logarithmic scaling automatically switches to linear when multiple
series or unsupported values are selected.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-19 16:58:35 +08:00

74 lines
2.3 KiB
TypeScript

export const checkHasNonPositiveValues = (
data: readonly Record<string, unknown>[],
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<string, unknown>[],
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.reduce((max, d) => {
const magnitude = Math.abs(Number(d[key]) || 0)
return magnitude > max ? magnitude : max
}, 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`
if (value >= 1_000)
return `${(value / 1_000).toLocaleString(undefined, { maximumFractionDigits: 1 })}k`
return value.toLocaleString()
}
export const getCumulativeResults = (
results: { rows: readonly any[] },
config: { yKey: string | string[] }
) => {
if (!results?.rows?.length) {
return []
}
const yKeys = Array.isArray(config.yKey) ? config.yKey : [config.yKey]
const cumulativeResults = results.rows.reduce((acc, row) => {
const prev = acc[acc.length - 1] || {}
const next = { ...row }
yKeys.forEach((yKey) => {
// Coerce to Number before adding: Postgres returns `bigint`, `numeric`,
// `money` and `count(*)` columns as strings, so a bare `+` would
// concatenate (e.g. "10" + "20" -> "1020") instead of summing.
next[yKey] = (Number(prev[yKey]) || 0) + (Number(row[yKey]) || 0)
})
return [...acc, next]
}, [])
return cumulativeResults
}