| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- import { LOCAL_STORAGE_KEYS, useParams } from 'common'
- import dayjs from 'dayjs'
- import { ArrowUpDown, X } from 'lucide-react'
- import Link from 'next/link'
- import { useMemo } from 'react'
- import {
- Badge,
- Button,
- Checkbox,
- Label,
- ResizableHandle,
- ResizablePanel,
- ResizablePanelGroup,
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- } from 'ui'
- import { Admonition } from 'ui-patterns'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import BarChart from '@/components/ui/Charts/BarChart'
- import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
- import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
- type Results = { rows: readonly any[] }
- export type ChartConfig = {
- view?: 'table' | 'chart'
- type: 'bar' | 'line'
- cumulative: boolean
- xKey: string
- yKey: string
- showLabels?: boolean
- showGrid?: boolean
- logScale?: boolean
- }
- const getCumulativeResults = (results: Results, config: ChartConfig) => {
- if (!results?.rows?.length) {
- return []
- }
- const cumulativeResults = results.rows.reduce((acc, row) => {
- const prev = acc[acc.length - 1] || {}
- const next = {
- ...row,
- [config.yKey]: (prev[config.yKey] || 0) + row[config.yKey],
- }
- return [...acc, next]
- }, [])
- return cumulativeResults
- }
- const VALID_RESULT_KEY_TYPES = ['number', 'string', 'date']
- type ChartConfigProps = {
- results: Results
- config: ChartConfig
- onConfigChange: (config: ChartConfig) => void
- }
- export const ChartConfig = ({
- results = { rows: [] },
- config,
- onConfigChange,
- }: ChartConfigProps) => {
- const { ref } = useParams()
- const [acknowledged, setAcknowledged] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.SQL_EDITOR_SQL_BLOCK_ACKNOWLEDGED(ref as string),
- false
- )
- // If a result key is not valid, it will be filtered out
- const resultKeys = useMemo(() => {
- return Object.keys(results.rows[0] || {}).filter((key) => {
- const type = typeof results.rows[0][key]
- return VALID_RESULT_KEY_TYPES.includes(type)
- })
- }, [results])
- // Only allow Y-axis keys that are numbers
- const yAxisKeys = useMemo(() => {
- if (!results.rows[0]) return []
- return Object.keys(results.rows[0]).filter((key) => {
- const value = results.rows[0][key]
- return typeof value === 'number' || !isNaN(Number(value))
- })
- }, [results])
- const hasConfig = config.xKey && config.yKey
- const canFlip = useMemo(() => {
- if (!hasConfig) return false
- const xKeyType = typeof results.rows[0]?.[config.xKey]
- const yKeyType = typeof results.rows[0]?.[config.yKey]
- return xKeyType === 'number' && yKeyType === 'number'
- }, [hasConfig, results.rows, config.xKey, config.yKey])
- // Compute cumulative results only if necessary
- const cumulativeResults = useMemo(() => getCumulativeResults(results, config), [results, config])
- const resultToRender = config.cumulative ? cumulativeResults : results.rows
- const getDateFormat = (key: any) => {
- const value = resultToRender?.[0]?.[key] || ''
- if (typeof value === 'number') return 'number'
- if (dayjs(value).isValid()) return 'date'
- return 'string'
- }
- const xKeyDateFormat = getDateFormat(config.xKey)
- const onFlip = () => {
- const newY = config.xKey
- const newX = config.yKey
- onConfigChange({ ...config, xKey: newX, yKey: newY })
- }
- if (!resultKeys.length) {
- return (
- <div className="p-2">
- <NoDataPlaceholder
- size="normal"
- description="Execute a query and configure the chart options."
- />
- </div>
- )
- }
- return (
- <ResizablePanelGroup orientation="horizontal" className="grow h-full">
- <ResizablePanel className="p-4 h-full" defaultSize="75">
- {!hasConfig ? (
- <ResizablePanel className="p-4 h-full" defaultSize="75">
- <NoDataPlaceholder
- size="normal"
- title="Configure your chart"
- description="Select your X and Y axis in the chart options panel"
- />
- </ResizablePanel>
- ) : config.type === 'bar' ? (
- <BarChart
- showLegend
- size="normal"
- xAxisIsDate={xKeyDateFormat === 'date'}
- data={resultToRender}
- xAxisKey={config.xKey}
- yAxisKey={config.yKey}
- showGrid={config.showGrid}
- XAxisProps={{
- angle: 0,
- interval: 'preserveStart',
- hide: !config.showLabels,
- tickFormatter: (idx: string) => {
- const value = resultToRender[+idx][config.xKey]
- if (xKeyDateFormat === 'date') {
- return dayjs(value).format('MMM D YYYY HH:mm')
- }
- return value
- },
- }}
- YAxisProps={{
- tickFormatter: (value: number) => value.toLocaleString(),
- hide: !config.showLabels,
- domain: [0, 'dataMax'],
- }}
- />
- ) : null}
- </ResizablePanel>
- <ResizableHandle withHandle />
- <ResizablePanel
- defaultSize="25"
- minSize="15"
- className="px-3 py-3 space-y-4 overflow-y-auto!"
- >
- <div className="flex justify-between items-center h-5">
- <h2 className="text-sm text-foreground-lighter">Chart options</h2>
- {config.xKey && config.yKey && (
- <ButtonTooltip
- type="text"
- size="tiny"
- onClick={onFlip}
- disabled={!canFlip}
- icon={<ArrowUpDown size="15" className="text-foreground-lighter" />}
- tooltip={{
- content: {
- side: 'bottom',
- className: 'w-64 text-center',
- text: canFlip
- ? 'Swap X and Y axis'
- : 'Unable to swap X and Y axis - both axes need to numerical values',
- },
- }}
- >
- Flip
- </ButtonTooltip>
- )}
- </div>
- {!acknowledged && (
- <Admonition showIcon={false} type="tip" className="p-2 relative group">
- <Tooltip>
- <TooltipTrigger
- onClick={() => setAcknowledged(true)}
- className="absolute top-3 right-3 opacity-30 group-hover:opacity-100 transition-opacity"
- >
- <X size={14} className="text-foreground-light" />
- </TooltipTrigger>
- <TooltipContent side="bottom">Dismiss</TooltipContent>
- </Tooltip>
- <div className="flex items-center gap-x-2">
- <Badge variant="success">New</Badge>
- <p className="text-xs">Add this chart to custom reports</p>
- </div>
- <p className="text-xs text-foreground-light mt-1!">
- SQL snippets can now be added and saved to your custom reports. Try it out now!
- </p>
- <Button asChild size="tiny" type="default" className="mt-1">
- <Link href={`/project/${ref}/reports`}>Head to Reports</Link>
- </Button>
- </Admonition>
- )}
- <div>
- <Label className="text-xs text-foreground-light">X Axis</Label>
- <Select
- value={config.xKey}
- onValueChange={(value) => {
- onConfigChange({ ...config, xKey: value })
- }}
- >
- <SelectTrigger>{config.xKey || 'Select X Axis'}</SelectTrigger>
- <SelectContent>
- <SelectGroup>
- {resultKeys.map((key) => (
- <SelectItem value={key} key={key}>
- {key}
- </SelectItem>
- ))}
- </SelectGroup>
- </SelectContent>
- </Select>
- </div>
- <div>
- <Label className="text-xs text-foreground-light">Y Axis</Label>
- <Select
- value={config.yKey}
- onValueChange={(value) => {
- onConfigChange({ ...config, yKey: value })
- }}
- >
- <SelectTrigger>{config.yKey || 'Select Y Axis'}</SelectTrigger>
- <SelectContent>
- <SelectGroup>
- {yAxisKeys.map((key) => (
- <SelectItem value={key} key={key}>
- {key}
- </SelectItem>
- ))}
- </SelectGroup>
- </SelectContent>
- </Select>
- </div>
- <div className="*:flex *:gap-2 *:items-center grid gap-2 *:text-foreground-light *:p-1.5 *:pl-0">
- <Label className="" htmlFor="cumulative">
- <Checkbox
- id="cumulative"
- name="cumulative"
- checked={config.cumulative}
- onClick={() => onConfigChange({ ...config, cumulative: !config.cumulative })}
- />
- Cumulative
- </Label>
- <Label htmlFor="showLabels">
- <Checkbox
- id="showLabels"
- name="showLabels"
- checked={config.showLabels}
- onClick={() => onConfigChange({ ...config, showLabels: !config.showLabels })}
- />
- Show labels
- </Label>
- <Label htmlFor="showGrid">
- <Checkbox
- id="showGrid"
- name="showGrid"
- checked={config.showGrid}
- onClick={() => onConfigChange({ ...config, showGrid: !config.showGrid })}
- />
- Show grid
- </Label>
- </div>
- </ResizablePanel>
- </ResizablePanelGroup>
- )
- }
|