Files
supabase/apps/studio/components/ui/Charts/Charts.utils.tsx
f65884f341 Reports consolidation 1: Edge Functions (#38129)
* wip

* add formatTimestamp for reports

* add tests for formatTimestamp

* fix type err

* rm old chart logic

* do not retry replication sources

* update report config schema

* use report config

* fix charts, simplify config

* sync charts

* rm comment

* usefilltimeseries

* move chart upsell to its own component

* cleanup v2 chart component

* fix chartheader function format bug

* make attributes dynamic

* move types to its own file

* update reportv2chart types

* update reportv2 chart type

* use new edgefn report

* rm mock

* rm old code

* fix type err

* fix type err

* rename hook

* return sql in dataprovider

* add button to open in log explorer in chart header

* fix type

* fix chart type toggle

* fix type error

* export data transformation for testing

* add tests for data transformation

* rm dupped code

* add report settings to edge fn report

* rm mocked data

* rename

* fix type err

* Apply suggestion from @joshenlim

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>

* rm unnecessary div

* rm unnecessary props from report upsell

* cleaner query keys

* move loadin/error/empty states to inside panel content to prevent layout shift

* use card instead of panel

* use buttontooltip in chartheader

* use buttontooltips in buttons with tooltips

* rm export default

* rm export defualt

* rm i from key

* fix status code transformation

* fix filltimeseries

* add tests for filltimeseries

* fix data transform

* pass xaxis key to chart

* refactor hover state to use timestamp not index

* fix tests

* fix areachart

* fix barchart

* fix chartheader

* fix composedchart

* fix stacked

* fix type missing xaxiskey

* type fix

* Revert "refactor hover state to use timestamp not index"

This reverts commit cfd7886460.

* undo charthover changes, fix data aggregation

* Revert "fix tests"

This reverts commit e5200a2035.

* move transforms to testable functions, add tests

* nit refactors and clean up

* feat: add aws nimbus cloud provider (#38312)

* feat: add aws nimbus cloud provider

* Fix return type of useCustomContent hook

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>

* add utc plugin to dayjs

* move plugins to vitest setup

* rm customparser plugin

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
2025-09-01 11:10:30 +02:00

168 lines
4.3 KiB
TypeScript

import dayjs from 'dayjs'
import { FC, PropsWithChildren, useMemo } from 'react'
import { ResponsiveContainer } from 'recharts'
import { DateTimeFormats } from './Charts.constants'
import type { CommonChartProps, StackedChartProps } from './Charts.types'
/**
* Auto formats a number to a default precision if it is a float
*
* @example
* numberFormatter(123) // "123"
* numberFormatter(123.123) // "123.12"
* numberFormatter(123, 2) // "123.00"
*/
export const numberFormatter = (num: number, precision = 2) =>
isFloat(num) ? precisionFormatter(num, precision) : num.toLocaleString()
/**
* Tests if a number is a float.
*
* @example
* isFloat(123) // false
* isFloat(123.123) // true
*/
export const isFloat = (num: number) => String(num).includes('.')
/**
* Formats a number to a particular precision.
*
* @example
* precisionFormatter(123, 2) // "123.00"
* precisionFormatter(123.123, 2) // "123.12"
*/
export const precisionFormatter = (num: number, precision: number): string => {
if (precision === 0) {
return String(Math.round(num))
}
if (isFloat(num)) {
const [head, tail] = String(num).split('.')
return Number(head).toLocaleString() + '.' + tail.slice(0, precision)
} else {
// pad int with 0
return num.toLocaleString() + '.' + '0'.repeat(precision)
}
}
/**
* Formats a timestamp.
* Optionally formats the string to UTC
* @param value
* @param format
* @param utc
* @returns
*/
export const timestampFormatter = (
value: string,
format: string = DateTimeFormats.FULL,
utc: boolean = false
) => {
if (utc) {
return dayjs.utc(value).format(format)
}
return dayjs(value).format(format)
}
/**
* Hook to create common wrapping components, perform data transformations
* returns a Container component and the minHeight set
*/
export const useChartSize = (
size: CommonChartProps<any>['size'] = 'normal',
sizeMap: {
tiny: number
small: number
normal: number
large: number
} = {
tiny: 76,
small: 96,
normal: 160,
large: 280,
}
) => {
const minHeight = sizeMap[size]
const Container: FC<PropsWithChildren & { className?: string }> = useMemo(
() =>
({ className, children }) => (
<ResponsiveContainer
className={className}
height={minHeight}
minHeight={minHeight}
width="100%"
>
{children as JSX.Element}
</ResponsiveContainer>
),
[size]
)
return {
Container,
minHeight,
}
}
/**
* Transforms data points into a stacked data structure that can be consumed by recharts
*/
export const useStacked = ({
data,
xAxisKey,
yAxisKey,
stackKey,
variant = 'values',
}: Pick<StackedChartProps<any>, 'xAxisKey' | 'yAxisKey' | 'stackKey'> & {
variant?: 'values' | 'percentages'
} & Pick<CommonChartProps<Record<string, number>>, 'data'>) => {
const stackedData = useMemo(() => {
if (!data) return []
const mapping = data.reduce(
(acc, datum) => {
const x = datum[xAxisKey]
const y = datum[yAxisKey]
const s = datum[stackKey]
if (!acc[x]) {
acc[x] = {}
}
acc[x][s] = y
return acc
},
{} as Record<string, Record<string, number>>
)
const flattened = Object.entries(mapping).map(([x, sMap]) => ({
...sMap,
[xAxisKey]: Number.isNaN(Number(x)) ? x : Number(x),
}))
return flattened
}, [JSON.stringify(data)])
const dataKeys = useMemo(() => {
return Object.keys(stackedData[0] || {})
.filter((k) => k !== xAxisKey && k !== yAxisKey)
.sort()
}, [JSON.stringify(stackedData[0] || {})])
const percentagesStackedData = useMemo(() => {
if (variant !== 'percentages') return
return stackedData.map((stack) => {
const entries = Object.entries(stack) as Array<[string, number]>
let map
const sum = entries
.filter(([key, _value]) => dataKeys.includes(key))
.reduce((acc, [_key, value]) => acc + value, 0)
map = entries.reduce((acc, [key, value]) => {
if (!dataKeys.includes(key)) {
return { ...acc, [key]: value }
}
return { ...acc, [key]: value !== 0 ? value / sum : 0 }
}, {} as any)
return map
})
}, [JSON.stringify(stackedData)])
return { dataKeys, stackedData, percentagesStackedData }
}