This is an automated email from the ASF dual-hosted git repository.
jackie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new b837df10291 [timeseries][ui] Add configurable max series limit for
timeseries chart visualization (#16580)
b837df10291 is described below
commit b837df10291f99ba3ddf7c09a490ab83400e93d9
Author: Shaurya Chaturvedi <[email protected]>
AuthorDate: Tue Aug 12 22:44:23 2025 -0700
[timeseries][ui] Add configurable max series limit for timeseries chart
visualization (#16580)
---
.../app/components/Query/TimeseriesChart.tsx | 18 ++----
.../app/components/Query/TimeseriesQueryPage.tsx | 71 ++++++++++++++++------
.../src/main/resources/app/utils/ChartConstants.ts | 4 +-
3 files changed, 58 insertions(+), 35 deletions(-)
diff --git
a/pinot-controller/src/main/resources/app/components/Query/TimeseriesChart.tsx
b/pinot-controller/src/main/resources/app/components/Query/TimeseriesChart.tsx
index 908f56c3e66..64361d77d01 100644
---
a/pinot-controller/src/main/resources/app/components/Query/TimeseriesChart.tsx
+++
b/pinot-controller/src/main/resources/app/components/Query/TimeseriesChart.tsx
@@ -22,7 +22,7 @@ import ReactECharts from 'echarts-for-react';
import { makeStyles } from '@material-ui/core/styles';
import { Typography, Paper } from '@material-ui/core';
import { ChartSeries } from 'Models';
-import { getSeriesColor, MAX_SERIES_LIMIT, CHART_PADDING_PERCENTAGE } from
'../../utils/ChartConstants';
+import { getSeriesColor, CHART_PADDING_PERCENTAGE } from
'../../utils/ChartConstants';
// Define proper types for ECharts parameters
interface EChartsTooltipParams {
@@ -33,9 +33,7 @@ interface EChartsTooltipParams {
// Extract chart configuration functions
const createChartSeries = (series: ChartSeries[], selectedMetric?: string) => {
- const limitedSeries = series.slice(0, MAX_SERIES_LIMIT);
-
- return limitedSeries.map((s, index) => {
+ return series.map((s, index) => {
const isSelected = selectedMetric ? s.name === selectedMetric : true;
return {
@@ -85,8 +83,7 @@ const createTooltipFormatter = (selectedMetric?: string) => {
};
const getTimeRange = (series: ChartSeries[]) => {
- const limitedSeries = series.slice(0, MAX_SERIES_LIMIT);
- const allTimestamps = limitedSeries.flatMap(s => s.data.map(dp =>
dp.timestamp));
+ const allTimestamps = series.flatMap(s => s.data.map(dp => dp.timestamp));
const minTime = Math.min(...allTimestamps);
const maxTime = Math.max(...allTimestamps);
return { minTime, maxTime };
@@ -234,18 +231,11 @@ const TimeseriesChart: React.FC<TimeseriesChartProps> = ({
);
}
- const hasMoreSeries = series.length > MAX_SERIES_LIMIT;
-
return (
<Paper className={classes.chartContainer} style={{ height: `${height}px`
}}>
- {hasMoreSeries && (
- <Typography variant="body2" color="textSecondary" style={{ padding:
'8px 16px', backgroundColor: '#fff3cd', borderBottom: '1px solid #ffeaa7' }}>
- Showing first {MAX_SERIES_LIMIT} of {series.length} series. Consider
filtering your query to improve performance.
- </Typography>
- )}
<ReactECharts
option={chartOption}
- style={{ height: hasMoreSeries ? 'calc(100% - 40px)' : '100%', width:
'100%' }}
+ style={{ height: '100%', width: '100%' }}
opts={{ renderer: 'canvas' }}
/>
</Paper>
diff --git
a/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
b/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
index 7e63d556883..720a97268ea 100644
---
a/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
+++
b/pinot-controller/src/main/resources/app/components/Query/TimeseriesQueryPage.tsx
@@ -47,7 +47,7 @@ import TimeseriesChart from './TimeseriesChart';
import MetricStatsTable from './MetricStatsTable';
import { parseTimeseriesResponse, isPrometheusFormat } from
'../../utils/TimeseriesUtils';
import { ChartSeries } from 'Models';
-import { MAX_SERIES_LIMIT } from '../../utils/ChartConstants';
+import { DEFAULT_SERIES_LIMIT } from '../../utils/ChartConstants';
// Define proper types
interface TimeseriesQueryResponse {
@@ -144,6 +144,20 @@ const useStyles = makeStyles((theme) => ({
padding: theme.spacing(1),
minWidth: 0,
},
+ seriesLimitContainer: {
+ display: 'flex',
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ padding: theme.spacing(1),
+ borderTop: `1px solid ${theme.palette.divider}`,
+ backgroundColor: theme.palette.grey[50],
+ },
+ seriesLimitInput: {
+ width: 50,
+ marginLeft: theme.spacing(2),
+ '& .MuiInputBase-input': { padding: '8px 12px', fontSize: '1rem' },
+ },
+ timeseriesAccordionDetails: { overflow: 'hidden !important' },
}));
@@ -157,8 +171,8 @@ const TruncationWarning: React.FC<{ totalSeries: number;
truncatedSeries: number
return (
<Alert severity="warning" style={{ marginBottom: '16px' }}>
<Typography variant="body2">
- Large dataset detected: Showing first {truncatedSeries} of
{totalSeries} series for visualization.
- Switch to JSON view to see the complete dataset.
+ Truncation warning: Showing first {truncatedSeries} of {totalSeries}
series for visualization.
+ Switch to JSON view to see the complete dataset or increase the max
series limit.
</Typography>
</Alert>
);
@@ -252,15 +266,15 @@ const TimeseriesQueryPage = () => {
const [languagesLoading, setLanguagesLoading] = useState(true);
const [rawOutput, setRawOutput] = useState<string>('');
- const [rawData, setRawData] = useState<TimeseriesQueryResponse | null>(null);
const [chartSeries, setChartSeries] = useState<ChartSeries[]>([]);
- const [truncatedChartSeries, setTruncatedChartSeries] =
useState<ChartSeries[]>([]);
+ const [totalSeriesCount, setTotalSeriesCount] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [shouldAutoExecute, setShouldAutoExecute] = useState<boolean>(false);
const [copyMsg, showCopyMsg] = React.useState(false);
const [viewType, setViewType] = useState<'json' | 'chart'>('chart');
const [selectedMetric, setSelectedMetric] = useState<string | null>(null);
+ const [seriesLimitInput, setSeriesLimitInput] =
useState<string>(DEFAULT_SERIES_LIMIT.toString());
// Fetch supported languages from controller configuration
@@ -351,30 +365,32 @@ const TimeseriesQueryPage = () => {
// Extract data processing logic
const processQueryResponse = useCallback((parsedData:
TimeseriesQueryResponse) => {
- setRawData(parsedData);
setRawOutput(JSON.stringify(parsedData, null, 2));
// Check if this is an error response
if (parsedData.error != null && parsedData.error !== '') {
setError(parsedData.error);
setChartSeries([]);
- setTruncatedChartSeries([]);
+ setTotalSeriesCount(0);
return;
}
// Parse timeseries data for chart and stats
if (isPrometheusFormat(parsedData)) {
const series = parseTimeseriesResponse(parsedData);
- setChartSeries(series);
+ setTotalSeriesCount(series.length);
- // Create truncated series for visualization (limit to MAX_SERIES_LIMIT)
- const truncatedSeries = series.slice(0, MAX_SERIES_LIMIT);
- setTruncatedChartSeries(truncatedSeries);
+ // Create truncated series for visualization (limit to seriesLimitInput
or default to DEFAULT_SERIES_LIMIT)
+ const limit = parseInt(seriesLimitInput, 10);
+ const effectiveLimit = !isNaN(limit) && limit > 0 ? limit :
DEFAULT_SERIES_LIMIT;
+
+ const truncatedSeries = series.slice(0, effectiveLimit);
+ setChartSeries(truncatedSeries);
} else {
setChartSeries([]);
- setTruncatedChartSeries([]);
+ setTotalSeriesCount(0);
}
- }, []);
+ }, [seriesLimitInput]);
const handleExecuteQuery = useCallback(async () => {
if (!config.query.trim()) {
@@ -386,6 +402,9 @@ const TimeseriesQueryPage = () => {
setIsLoading(true);
setError('');
setRawOutput('');
+ setSelectedMetric(null);
+ setChartSeries([]);
+ setTotalSeriesCount(0);
try {
const requestPayload = {
@@ -544,7 +563,7 @@ const TimeseriesQueryPage = () => {
<ViewToggle
viewType={viewType}
onViewChange={setViewType}
- isChartDisabled={truncatedChartSeries.length === 0}
+ isChartDisabled={chartSeries.length === 0}
onCopy={copyToClipboard}
copyMsg={copyMsg}
classes={classes}
@@ -560,23 +579,37 @@ const TimeseriesQueryPage = () => {
<SimpleAccordion
headerTitle="Timeseries Chart & Statistics"
showSearchBox={false}
+ detailsContainerClass={classes.timeseriesAccordionDetails}
>
- {truncatedChartSeries.length > 0 ? (
+ {chartSeries.length > 0 ? (
<>
<TruncationWarning
- totalSeries={chartSeries.length}
- truncatedSeries={truncatedChartSeries.length}
+ totalSeries={totalSeriesCount}
+ truncatedSeries={chartSeries.length}
/>
<TimeseriesChart
- series={truncatedChartSeries}
+ series={chartSeries}
height={500}
selectedMetric={selectedMetric}
/>
<MetricStatsTable
- series={truncatedChartSeries}
+ series={chartSeries}
selectedMetric={selectedMetric}
onMetricSelect={setSelectedMetric}
/>
+ <div className={classes.seriesLimitContainer}>
+ <Typography variant="body2" color="textSecondary">
+ Max Series Render Limit:
+ </Typography>
+ <FormControl className={classes.seriesLimitInput}>
+ <Input
+ value={seriesLimitInput}
+ onChange={(e) => setSeriesLimitInput(e.target.value)}
+ inputProps={{ min: 1, max: 1000 }}
+ placeholder={DEFAULT_SERIES_LIMIT.toString()}
+ />
+ </FormControl>
+ </div>
</>
) : (
<Box p={3} textAlign="center">
diff --git a/pinot-controller/src/main/resources/app/utils/ChartConstants.ts
b/pinot-controller/src/main/resources/app/utils/ChartConstants.ts
index 5e1f823c851..c5277087122 100644
--- a/pinot-controller/src/main/resources/app/utils/ChartConstants.ts
+++ b/pinot-controller/src/main/resources/app/utils/ChartConstants.ts
@@ -26,9 +26,9 @@ export const CHART_COLORS = [
];
/**
- * Maximum number of series that can be rendered in the chart
+ * Default number of series that can be rendered in the chart
*/
-export const MAX_SERIES_LIMIT = 20;
+export const DEFAULT_SERIES_LIMIT = 40;
/**
* Chart padding percentage for time axis and series
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]