From 2a7c3db3f0548566546d0588638377846325f35f Mon Sep 17 00:00:00 2001 From: Shorpo <149748269+svc-shorpo@users.noreply.github.com> Date: Sun, 3 Mar 2024 03:10:54 -0700 Subject: [PATCH 1/6] chore: Remove deprecated HDXLineChart and HDXMultiSeriesTimeChart (#329) Going to remove `NumberChart` and `api.useMetricsChart` in https://github.com/hyperdxio/hyperdx/pull/314 --- packages/app/src/HDXLineChart.tsx | 626 ------------------- packages/app/src/HDXMultiSeriesTimeChart.tsx | 38 +- packages/app/src/HDXTableChart.tsx | 310 --------- 3 files changed, 37 insertions(+), 937 deletions(-) delete mode 100644 packages/app/src/HDXLineChart.tsx delete mode 100644 packages/app/src/HDXTableChart.tsx diff --git a/packages/app/src/HDXLineChart.tsx b/packages/app/src/HDXLineChart.tsx deleted file mode 100644 index 2886cdc67..000000000 --- a/packages/app/src/HDXLineChart.tsx +++ /dev/null @@ -1,626 +0,0 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import Link from 'next/link'; -import cx from 'classnames'; -import { add, format } from 'date-fns'; -import pick from 'lodash/pick'; -import { ErrorBoundary, withErrorBoundary } from 'react-error-boundary'; -import { toast } from 'react-toastify'; -import { - Bar, - BarChart, - Label, - Legend, - Line, - LineChart, - ReferenceArea, - ReferenceLine, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from 'recharts'; -import { Popover } from '@mantine/core'; - -import api from './api'; -import { convertGranularityToSeconds, Granularity } from './ChartUtils'; -import type { AggFn, NumberFormat } from './types'; -import useUserPreferences, { TimeFormat } from './useUserPreferences'; -import { formatNumber } from './utils'; -import { semanticKeyedColor, TIME_TOKENS, truncateMiddle } from './utils'; - -import styles from '../styles/HDXLineChart.module.scss'; - -const MAX_LEGEND_ITEMS = 4; - -function CopyableLegendItem({ entry }: any) { - return ( - { - window.navigator.clipboard.writeText(entry.value); - toast.success(`Copied to clipboard`); - }} - title="Click to expand" - > - - {entry.value} - - ); -} - -function ExpandableLegendItem({ entry, expanded }: any) { - const [_expanded, setExpanded] = useState(false); - const isExpanded = _expanded || expanded; - - return ( - setExpanded(v => !v)} - title="Click to expand" - > - - {isExpanded ? entry.value : truncateMiddle(`${entry.value}`, 35)} - - ); -} - -export const LegendRenderer = memo<{ - payload?: { - value: string; - color: string; - }[]; -}>(props => { - const payload = props.payload ?? []; - const shownItems = payload.slice(0, MAX_LEGEND_ITEMS); - const restItems = payload.slice(MAX_LEGEND_ITEMS); - - return ( -
- {shownItems.map((entry, index) => ( - - ))} - {restItems.length ? ( - - -
- +{restItems.length} more -
-
- -
- {restItems.map((entry, index) => ( - - ))} -
-
-
- ) : null} -
- ); -}); - -const HARD_LINES_LIMIT = 60; -const MemoChart = memo(function MemoChart({ - graphResults, - setIsClickActive, - isClickActive, - dateRange, - groupKeys, - alertThreshold, - alertThresholdType, - logReferenceTimestamp, - displayType = 'line', - numberFormat, -}: { - graphResults: any[]; - setIsClickActive: (v: any) => void; - isClickActive: any; - dateRange: [Date, Date]; - groupKeys: string[]; - alertThreshold?: number; - alertThresholdType?: 'above' | 'below'; - displayType?: 'stacked_bar' | 'line'; - numberFormat?: NumberFormat; - logReferenceTimestamp?: number; -}) { - const ChartComponent = displayType === 'stacked_bar' ? BarChart : LineChart; - - const lines = useMemo(() => { - return groupKeys - .slice(0, HARD_LINES_LIMIT) - .map(key => - displayType === 'stacked_bar' ? ( - - ) : ( - - ), - ); - }, [groupKeys, displayType]); - - const sizeRef = useRef<[number, number]>([0, 0]); - const timeFormat: TimeFormat = useUserPreferences().timeFormat; - const tsFormat = TIME_TOKENS[timeFormat]; - // Gets the preffered time format from User Preferences, then converts it to a formattable token - - const tickFormatter = useCallback( - (value: number) => - numberFormat - ? formatNumber(value, { - ...numberFormat, - average: true, - mantissa: 0, - unit: undefined, - }) - : new Intl.NumberFormat('en-US', { - notation: 'compact', - compactDisplay: 'short', - }).format(value), - [numberFormat], - ); - - return ( - { - sizeRef.current = [width ?? 1, height ?? 1]; - }} - > - { - if ( - state != null && - state.chartX != null && - state.chartY != null && - state.activeLabel != null - ) { - setIsClickActive({ - x: state.chartX, - y: state.chartY, - activeLabel: state.activeLabel, - xPerc: state.chartX / sizeRef.current[0], - yPerc: state.chartY / sizeRef.current[1], - }); - } else { - // We clicked on the chart but outside of a line - setIsClickActive(undefined); - } - - // TODO: Properly detect clicks outside of the fake tooltip - e.stopPropagation(); - }} - > - format(new Date(tick * 1000), tsFormat)} - minTickGap={50} - tick={{ fontSize: 12, fontFamily: 'IBM Plex Mono, monospace' }} - /> - - {lines} - } - /> - {alertThreshold != null && alertThresholdType === 'below' && ( - - )} - {alertThreshold != null && alertThresholdType === 'above' && ( - - )} - {alertThreshold != null && ( - } - stroke="red" - strokeDasharray="3 3" - /> - )} - } - /> - {/** Needs to be at the bottom to prevent re-rendering */} - {isClickActive != null ? ( - - ) : null} - {logReferenceTimestamp != null ? ( - - ) : null} - - - ); -}); - -export const HDXLineChartTooltip = withErrorBoundary( - memo((props: any) => { - const timeFormat: TimeFormat = useUserPreferences().timeFormat; - const tsFormat = TIME_TOKENS[timeFormat]; - const { active, payload, label, numberFormat } = props; - if (active && payload && payload.length) { - return ( -
-
- {format(new Date(label * 1000), tsFormat)} -
-
- {payload - .sort((a: any, b: any) => b.value - a.value) - .map((p: any) => ( -
- {truncateMiddle(p.name ?? p.dataKey, 70)}:{' '} - {numberFormat ? formatNumber(p.value, numberFormat) : p.value} -
- ))} -
-
- ); - } - return null; - }), - { - onError: console.error, - fallback: ( -
- An error occurred while rendering the tooltip. -
- ), - }, -); - -/** - * @deprecated Use HDXMultiSeriesTimeChart instead - */ -const HDXLineChart = memo( - ({ - config: { - table, - aggFn, - field, - where, - groupBy, - granularity, - dateRange, - numberFormat, - }, - onSettled, - alertThreshold, - alertThresholdType, - logReferenceTimestamp, - }: { - config: { - table: string; - aggFn: AggFn; - field: string; - where: string; - groupBy: string; - granularity: Granularity; - dateRange: [Date, Date]; - numberFormat?: NumberFormat; - }; - onSettled?: () => void; - alertThreshold?: number; - alertThresholdType?: 'above' | 'below'; - logReferenceTimestamp?: number; - }) => { - const { data, isError, isLoading } = - table === 'logs' - ? api.useLogsChart( - { - aggFn, - endDate: dateRange[1] ?? new Date(), - field, - granularity, - groupBy, - q: where, - startDate: dateRange[0] ?? new Date(), - }, - { - enabled: - aggFn === 'count' || - (typeof field === 'string' && field.length > 0), - onSettled, - }, - ) - : api.useMetricsChart( - { - aggFn, - endDate: dateRange[1] ?? new Date(), - granularity, - groupBy, - name: field, - q: where, - startDate: dateRange[0] ?? new Date(), - }, - { - onSettled, - }, - ); - - const tsBucketMap = new Map(); - let graphResults: { - ts_bucket: number; - [key: string]: number | undefined; - }[] = []; - let groupKeys: string[] = []; - const groupKeySet = new Set(); - const groupKeyMax = new Map(); - let totalGroups = 0; - if (data != null) { - for (const row of data.data) { - const key = row.group; - const value = Number.parseFloat(row.data); - - // Keep track of the max value we've seen for this key so far - // we'll pick the top N to display later - groupKeyMax.set(key, Math.max(groupKeyMax.get(key) ?? 0, value)); - - const tsBucket = tsBucketMap.get(row.ts_bucket) ?? {}; - groupKeySet.add(key); - tsBucketMap.set(row.ts_bucket, { - ...tsBucket, - ts_bucket: row.ts_bucket, - [key]: value, // CH can return strings for UInt64 - }); - } - - // get top N keys from groupKeyMax - const topN = 20; - const topNKeys = Array.from(groupKeyMax.entries()) - .filter(([groupKey]) => groupKey !== '') // filter out zero padding key - .sort((a, b) => b[1] - a[1]) - .slice(0, topN) - .map(([k]) => k); - - totalGroups = groupKeyMax.size - 1; // subtract zero padding key from total - - graphResults = Array.from(tsBucketMap.values()) - .sort((a, b) => a.ts_bucket - b.ts_bucket) - .map(v => { - const defaultValues = Object.fromEntries( - topNKeys.map(k => [k, aggFn === 'count' ? 0 : undefined]), // fill in undefined for missing value - ) as { [key: string]: number | undefined }; - return { - ...defaultValues, - ...(pick(v, ['ts_bucket', ...topNKeys]) as { - ts_bucket: number; - [key: string]: number; - }), - }; - }); - groupKeys = topNKeys; - } - - const [activeClickPayload, setActiveClickPayload] = useState< - | { - x: number; - y: number; - activeLabel: string; - xPerc: number; - yPerc: number; - } - | undefined - >(undefined); - - useEffect(() => { - const onClickHandler = () => { - if (activeClickPayload) { - setActiveClickPayload(undefined); - } - }; - document.addEventListener('click', onClickHandler); - return () => document.removeEventListener('click', onClickHandler); - }, [activeClickPayload]); - - const clickedActiveLabelDate = - activeClickPayload?.activeLabel != null - ? new Date(Number.parseInt(activeClickPayload.activeLabel) * 1000) - : undefined; - - let qparams: URLSearchParams | undefined; - - if (clickedActiveLabelDate != null) { - const to = add(clickedActiveLabelDate, { - seconds: convertGranularityToSeconds(granularity), - }); - qparams = new URLSearchParams({ - q: - where + - (aggFn !== 'count' ? ` ${field}:*` : '') + - (groupBy != null && groupBy != '' ? ` ${groupBy}:*` : ''), - from: `${clickedActiveLabelDate?.getTime()}`, - to: `${to.getTime()}`, - }); - } - - const [displayType, setDisplayType] = useState<'stacked_bar' | 'line'>( - 'line', - ); - - return isLoading ? ( -
- Loading Chart Data... -
- ) : isError ? ( -
- Error loading chart, please try again or contact support. -
- ) : graphResults.length === 0 ? ( -
- No data found within time range. -
- ) : ( -
-
- {activeClickPayload != null && clickedActiveLabelDate != null ? ( -
0.5 - ? (activeClickPayload?.x ?? 0) - 130 - : (activeClickPayload?.x ?? 0) + 4 - }px, ${activeClickPayload?.y ?? 0}px)`, - }} - > - - View Events - -
- ) : null} - {totalGroups > groupKeys.length ? ( -
- - Only top{' '} - {groupKeys.length} groups shown - -
- ) : null} -
- setDisplayType('line')} - > - - - setDisplayType('stacked_bar')} - > - - -
- -
-
- ); - }, -); - -export default HDXLineChart; diff --git a/packages/app/src/HDXMultiSeriesTimeChart.tsx b/packages/app/src/HDXMultiSeriesTimeChart.tsx index 44a163add..1dad62b71 100644 --- a/packages/app/src/HDXMultiSeriesTimeChart.tsx +++ b/packages/app/src/HDXMultiSeriesTimeChart.tsx @@ -2,6 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Link from 'next/link'; import cx from 'classnames'; import { add, format } from 'date-fns'; +import { withErrorBoundary } from 'react-error-boundary'; import { toast } from 'react-toastify'; import { Bar, @@ -26,7 +27,6 @@ import { seriesColumns, seriesToUrlSearchQueryParam, } from './ChartUtils'; -import { HDXLineChartTooltip } from './HDXLineChart'; import type { ChartSeries, NumberFormat } from './types'; import useUserPreferences, { TimeFormat } from './useUserPreferences'; import { formatNumber } from './utils'; @@ -36,6 +36,42 @@ import styles from '../styles/HDXLineChart.module.scss'; const MAX_LEGEND_ITEMS = 4; +const HDXLineChartTooltip = withErrorBoundary( + memo((props: any) => { + const timeFormat: TimeFormat = useUserPreferences().timeFormat; + const tsFormat = TIME_TOKENS[timeFormat]; + const { active, payload, label, numberFormat } = props; + if (active && payload && payload.length) { + return ( +
+
+ {format(new Date(label * 1000), tsFormat)} +
+
+ {payload + .sort((a: any, b: any) => b.value - a.value) + .map((p: any) => ( +
+ {truncateMiddle(p.name ?? p.dataKey, 70)}:{' '} + {numberFormat ? formatNumber(p.value, numberFormat) : p.value} +
+ ))} +
+
+ ); + } + return null; + }), + { + onError: console.error, + fallback: ( +
+ An error occurred while rendering the tooltip. +
+ ), + }, +); + function CopyableLegendItem({ entry }: any) { return ( void; -}) => { - //we need a reference to the scrolling element for logic down below - const tableContainerRef = useRef(null); - - const columns: ColumnDef[] = [ - { - accessorKey: 'group', - header: 'Group', - size: - // TODO: Figure out how to make this more robust - tableContainerRef.current?.clientWidth != null - ? tableContainerRef.current?.clientWidth * 0.5 - : 200, - }, - { - accessorKey: 'data', - header: valueColumnName, - size: UNDEFINED_WIDTH, - cell: ({ getValue }) => { - const value = getValue() as string; - if (numberFormat) { - return formatNumber(parseInt(value), numberFormat); - } - return value; - }, - }, - ]; - - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - enableColumnResizing: true, - columnResizeMode: 'onChange', - }); - - const { rows } = table.getRowModel(); - - const rowVirtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => tableContainerRef.current, - estimateSize: useCallback(() => 58, []), - overscan: 10, - paddingEnd: 20, - }); - - const items = rowVirtualizer.getVirtualItems(); - - const [paddingTop, paddingBottom] = - items.length > 0 - ? [ - Math.max(0, items[0].start - rowVirtualizer.options.scrollMargin), - Math.max( - 0, - rowVirtualizer.getTotalSize() - items[items.length - 1].end, - ), - ] - : [0, 0]; - - return ( -
- - - {table.getHeaderGroups().map(headerGroup => ( - - {headerGroup.headers.map((header, headerIndex) => { - return ( - - ); - })} - - ))} - - - {paddingTop > 0 && ( - - - )} - {items.map(virtualRow => { - const row = rows[virtualRow.index] as TableRow; - return ( - onRowClick?.(row.original)} - key={virtualRow.key} - className={cx('bg-default-dark-grey-hover', { - // 'bg-light-grey': highlightedPatternId === row.original.id, - })} - data-index={virtualRow.index} - ref={rowVirtualizer.measureElement} - > - {row.getVisibleCells().map(cell => { - return ( - - ); - })} - - ); - })} - {paddingBottom > 0 && ( - - - )} - -
- {header.isPlaceholder ? null : ( -
- {flexRender( - header.column.columnDef.header, - header.getContext(), - )} -
- )} - {header.column.getCanResize() && ( -
- -
- )} -
-
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} -
-
-
- ); -}; - -const HDXTableChart = memo( - ({ - config: { - table, - aggFn, - field, - where, - groupBy, - dateRange, - sortOrder, - numberFormat, - }, - onSettled, - }: { - config: { - table: string; - aggFn: AggFn; - field: string; - where: string; - groupBy: string; - dateRange: [Date, Date]; - sortOrder: 'asc' | 'desc'; - numberFormat?: NumberFormat; - }; - onSettled?: () => void; - }) => { - const { data, isError, isLoading } = - table === 'logs' - ? api.useLogsChart( - { - aggFn, - endDate: dateRange[1] ?? new Date(), - field, - granularity: undefined, - groupBy, - q: where, - startDate: dateRange[0] ?? new Date(), - sortOrder, - }, - { - onSettled, - }, - ) - : api.useMetricsChart( - { - aggFn, - endDate: dateRange[1] ?? new Date(), - granularity: undefined, - name: field, - q: where, - startDate: dateRange[0] ?? new Date(), - groupBy, - // sortOrder, - }, - { - onSettled, - }, - ); - - const valueColumnName = aggFn === 'count' ? 'Count' : `${aggFn}(${field})`; - - const router = useRouter(); - const handleRowClick = useMemo(() => { - if (table !== 'logs') { - return undefined; - } - return (row?: { group: string }) => { - const qparams = new URLSearchParams({ - q: - where + - (groupBy - ? ` ${groupBy}:${row?.group ? `"${row.group}"` : '*'}` - : ''), - from: `${dateRange[0].getTime()}`, - to: `${dateRange[1].getTime()}`, - }); - router.push(`/search?${qparams.toString()}`); - }; - }, [dateRange, groupBy, router, table, where]); - - return isLoading ? ( -
- Loading Chart Data... -
- ) : isError ? ( -
- Error loading chart, please try again or contact support. -
- ) : data?.data?.length === 0 ? ( -
- No data found within time range. -
- ) : ( -
- - - ); - }, -); - -export default HDXTableChart; From a7abd2c2f3de5c466440e31ffc82150df058aff5 Mon Sep 17 00:00:00 2001 From: Shorpo <149748269+svc-shorpo@users.noreply.github.com> Date: Sun, 3 Mar 2024 03:13:46 -0700 Subject: [PATCH 2/6] feat: Allow metrics and ratios in NumberChart (#314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![Screenshot 2024-02-19 at 11 37 52 PM](https://github.com/hyperdxio/hyperdx/assets/149748269/74a28fd3-8a25-46c6-af88-85762cc51b5e) ![Screenshot 2024-02-19 at 11 37 57 PM](https://github.com/hyperdxio/hyperdx/assets/149748269/ff57756a-ed73-4f9c-bac3-20c265751447) ![Screenshot 2024-02-19 at 11 33 42 PM](https://github.com/hyperdxio/hyperdx/assets/149748269/beebcaf4-f6b1-423c-9c3b-580ddeb1bfe0) ![Screenshot 2024-02-19 at 11 35 08 PM](https://github.com/hyperdxio/hyperdx/assets/149748269/631159ec-d2ec-45cd-97db-0d2243675c93) - [x] make sure backwards compatible - [x] make sure previously created number charts can be edited - [x] make sure it works on chart explorer page --- packages/api/src/clickhouse/index.ts | 8 +- packages/app/src/ChartUtils.tsx | 17 ++ packages/app/src/DashboardPage.tsx | 14 +- packages/app/src/EditChartForm.tsx | 254 +++++++++------------------ packages/app/src/HDXNumberChart.tsx | 58 +++--- 5 files changed, 136 insertions(+), 215 deletions(-) diff --git a/packages/api/src/clickhouse/index.ts b/packages/api/src/clickhouse/index.ts index 1f0cd38a3..7add58cb4 100644 --- a/packages/api/src/clickhouse/index.ts +++ b/packages/api/src/clickhouse/index.ts @@ -1715,7 +1715,7 @@ export const getMultiSeriesChart = async ({ queries = await Promise.all( series.map(s => { - if (s.type != 'time' && s.type != 'table') { + if (s.type != 'time' && s.type != 'table' && s.type != 'number') { throw new Error(`Unsupported series type: ${s.type}`); } if (s.table != 'logs' && s.table != null) { @@ -1727,7 +1727,7 @@ export const getMultiSeriesChart = async ({ endTime, field: s.field, granularity, - groupBy: s.groupBy, + groupBy: s.type === 'number' ? [] : s.groupBy, propertyTypeMappingsModel, q: s.where, sortOrder: s.type === 'table' ? s.sortOrder : undefined, @@ -1746,7 +1746,7 @@ export const getMultiSeriesChart = async ({ queries = await Promise.all( series.map(s => { - if (s.type != 'time' && s.type != 'table') { + if (s.type != 'time' && s.type != 'table' && s.type != 'number') { throw new Error(`Unsupported series type: ${s.type}`); } if (s.table != 'metrics') { @@ -1764,13 +1764,13 @@ export const getMultiSeriesChart = async ({ endTime, name: s.field, granularity, - groupBy: s.groupBy, sortOrder: s.type === 'table' ? s.sortOrder : undefined, q: s.where, startTime, teamId, dataType: s.metricDataType, propertyTypeMappingsModel, + groupBy: s.type === 'number' ? [] : s.groupBy, }); }), ); diff --git a/packages/app/src/ChartUtils.tsx b/packages/app/src/ChartUtils.tsx index 6604c8703..18b761fb6 100644 --- a/packages/app/src/ChartUtils.tsx +++ b/packages/app/src/ChartUtils.tsx @@ -77,6 +77,23 @@ export enum Granularity { ThirtyDay = '30 day', } +export const GRANULARITY_SECONDS_MAP: Record = { + [Granularity.ThirtySecond]: 30, + [Granularity.OneMinute]: 60, + [Granularity.FiveMinute]: 5 * 60, + [Granularity.TenMinute]: 10 * 60, + [Granularity.FifteenMinute]: 15 * 60, + [Granularity.ThirtyMinute]: 30 * 60, + [Granularity.OneHour]: 60 * 60, + [Granularity.TwoHour]: 2 * 60 * 60, + [Granularity.SixHour]: 6 * 60 * 60, + [Granularity.TwelveHour]: 12 * 60 * 60, + [Granularity.OneDay]: 24 * 60 * 60, + [Granularity.TwoDay]: 2 * 24 * 60 * 60, + [Granularity.SevenDay]: 7 * 24 * 60 * 60, + [Granularity.ThirtyDay]: 30 * 24 * 60 * 60, +}; + export const isGranularity = (value: string): value is Granularity => { return Object.values(Granularity).includes(value as Granularity); }; diff --git a/packages/app/src/DashboardPage.tsx b/packages/app/src/DashboardPage.tsx index 81280b234..2c3458d77 100644 --- a/packages/app/src/DashboardPage.tsx +++ b/packages/app/src/DashboardPage.tsx @@ -189,12 +189,18 @@ const Tile = forwardRef( : type === 'number' ? { type, - table: chart.series[0].table ?? 'logs', - aggFn: chart.series[0].aggFn, field: chart.series[0].field ?? '', // TODO: Fix in definition - where: buildAndWhereClause(query, chart.series[0].where), - dateRange, numberFormat: chart.series[0].numberFormat, + series: chart.series.map(s => ({ + ...s, + where: buildAndWhereClause( + query, + s.type === 'number' ? s.where : '', + ), + })), + dateRange, + granularity: + granularity ?? convertDateRangeToGranularityString(dateRange, 60), } : { type, diff --git a/packages/app/src/EditChartForm.tsx b/packages/app/src/EditChartForm.tsx index 7c618c26a..4b3951535 100644 --- a/packages/app/src/EditChartForm.tsx +++ b/packages/app/src/EditChartForm.tsx @@ -370,12 +370,11 @@ export const EditNumberChartForm = ({ const chartConfig = useMemo(() => { return _editedChart != null && _editedChart.series[0].type === 'number' ? { - aggFn: _editedChart.series[0].aggFn ?? 'count', - table: _editedChart.series[0].table ?? 'logs', field: _editedChart.series[0].field ?? '', // TODO: Fix in definition - where: _editedChart.series[0].where, dateRange, numberFormat: _editedChart.series[0].numberFormat, + series: _editedChart.series, + granularity: convertDateRangeToGranularityString(dateRange, 60), } : null; }, [_editedChart, dateRange]); @@ -393,9 +392,6 @@ export const EditNumberChartForm = ({ return null; } - const labelWidth = 320; - const aggFn = _editedChart.series[0].aggFn ?? 'count'; - return (
{ @@ -419,105 +415,14 @@ export const EditNumberChartForm = ({ placeholder="Chart Name" /> -
-
- Aggregation Function -
-
-