diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx
index 91f3d9945f3..d2020e4c562 100644
--- a/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx
+++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.test.tsx
@@ -1,74 +1,127 @@
-import { render } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
-import { createTheme, FieldSparkline, FieldType } from '@grafana/data';
+import { createTheme, Field, FieldSparkline, FieldType } from '@grafana/data';
import { Sparkline } from './Sparkline';
describe('Sparkline', () => {
- it('should render without throwing an error', () => {
- const sparkline: FieldSparkline = {
- x: {
- name: 'x',
- values: [1679839200000, 1680444000000, 1681048800000, 1681653600000, 1682258400000],
- type: FieldType.time,
- config: {},
- },
- y: {
- name: 'y',
- values: [1, 2, 3, 4, 5],
- type: FieldType.number,
- config: {},
- state: {
- range: { min: 1, max: 5, delta: 1 },
+ describe('renders without error', () => {
+ const numField = (name: string, numVals: number): Field => ({
+ name,
+ values: Array.from({ length: numVals }, (_, i) => i + 1),
+ type: FieldType.number,
+ config: {},
+ state:
+ numVals > 0
+ ? {
+ range: { min: 1, max: numVals, delta: numVals - 1 },
+ }
+ : {},
+ });
+
+ const startTime = 1679839200000;
+ const timeField = (name: string, numVals: number): Field => ({
+ name,
+ values: Array.from({ length: numVals }, (_, i) => startTime + (i + 1) * 1000),
+ type: FieldType.time,
+ config: {},
+ state:
+ numVals > 0
+ ? {
+ range: { min: 1, max: numVals, delta: numVals - 1 },
+ }
+ : {},
+ });
+
+ it.each<{ description: string; input: FieldSparkline; warning?: boolean }>([
+ {
+ description: 'x=time, y=number, 5 values',
+ input: {
+ x: timeField('x', 5),
+ y: numField('y', 5),
},
},
- };
- expect(() =>
- render()
- ).not.toThrow();
- });
-
- it('should not throw an error if there is a single value', () => {
- const sparkline: FieldSparkline = {
- x: {
- name: 'x',
- values: [1679839200000],
- type: FieldType.time,
- config: {},
+ {
+ description: 'x=time, y=number, 1 value',
+ input: {
+ x: timeField('x', 1),
+ y: numField('y', 1),
+ },
+ warning: true,
},
- y: {
- name: 'y',
- values: [1],
- type: FieldType.number,
- config: {},
- state: {
- range: { min: 1, max: 1, delta: 0 },
+ {
+ description: 'x=time, y=number, 0 values',
+ input: {
+ x: timeField('x', 0),
+ y: numField('y', 0),
+ },
+ warning: true,
+ },
+ {
+ description: 'x=time (unordered), y=number, 5 values',
+ input: {
+ x: {
+ ...timeField('x', 5),
+ values: timeField('x', 5).values.reverse(),
+ },
+ y: timeField('y', 5),
+ },
+ warning: true,
+ },
+ {
+ description: 'x=number, y=number, 5 values',
+ input: {
+ x: numField('x', 5),
+ y: numField('y', 5),
},
},
- };
- expect(() =>
- render()
- ).not.toThrow();
- });
+ {
+ description: 'x=number, y=number, 1 value',
+ input: {
+ x: numField('x', 1),
+ y: numField('y', 1),
+ },
+ warning: true,
+ },
+ {
+ description: 'x=number, y=number, 0 values',
+ input: {
+ x: numField('x', 0),
+ y: numField('y', 0),
+ },
+ warning: true,
+ },
+ {
+ description: 'y=number, 5 values',
+ input: {
+ y: numField('y', 5),
+ },
+ },
+ {
+ description: 'y=number, 1 value',
+ input: {
+ y: numField('y', 1),
+ },
+ warning: true,
+ },
+ {
+ description: 'y=number, 0 values',
+ input: {
+ y: numField('y', 0),
+ },
+ warning: true,
+ },
+ ])('does not throw for "$description"', ({ input, warning }) => {
+ expect(() =>
+ render()
+ ).not.toThrow();
- it('should not throw an error if there are no values', () => {
- const sparkline: FieldSparkline = {
- x: {
- name: 'x',
- values: [],
- type: FieldType.time,
- config: {},
- },
- y: {
- name: 'y',
- values: [],
- type: FieldType.number,
- config: {},
- state: {},
- },
- };
- expect(() =>
- render()
- ).not.toThrow();
+ if (warning) {
+ expect(screen.getByRole('alert')).toBeInTheDocument();
+ } else {
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ expect(screen.getByTestId('uplot-main-div')).toBeInTheDocument();
+ }
+ });
});
});
diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
index 68b1a5a832f..6975db48b28 100644
--- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
+++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
@@ -1,9 +1,14 @@
+import { css } from '@emotion/css';
import React, { memo } from 'react';
-import { FieldConfig, FieldSparkline } from '@grafana/data';
+import { colorManipulator, FieldConfig, FieldSparkline, GrafanaTheme2 } from '@grafana/data';
+import { Trans } from '@grafana/i18n';
import { GraphFieldConfig } from '@grafana/schema';
+import { useStyles2 } from '../../themes/ThemeContext';
import { Themeable2 } from '../../types/theme';
+import { Icon } from '../Icon/Icon';
+import { Tooltip } from '../Tooltip/Tooltip';
import { UPlotChart } from '../uPlot/Plot';
import { preparePlotData2, getStackingGroups } from '../uPlot/utils';
@@ -16,12 +21,59 @@ export interface SparklineProps extends Themeable2 {
sparkline: FieldSparkline;
}
+const CompactAlert = ({ children, width }: { width: number; children: string | React.ReactElement }) => {
+ const styles = useStyles2(getCompactAlertStyles);
+
+ return (
+
+ {width >= 400 ? (
+
+
+ {children}
+
+ ) : (
+
+
+
+ Cannot render sparkline
+
+
+ )}
+
+ );
+};
+
+const getCompactAlertStyles = (theme: GrafanaTheme2) => ({
+ content: css({
+ margin: theme.spacing(1),
+ fontSize: theme.typography.bodySmall.fontSize,
+ fontWeight: theme.typography.fontWeightMedium,
+ padding: theme.spacing(0.5, 1),
+ color: theme.colors.warning.contrastText,
+ background: colorManipulator.alpha(theme.colors.warning.main, 0.85),
+ borderRadius: theme.shape.radius.default,
+ display: 'flex',
+ justifyContent: 'center',
+
+ '& a': {
+ color: theme.colors.warning.contrastText,
+ textDecoration: 'underline',
+ '&:hover': {
+ textDecoration: 'none',
+ },
+ },
+ }),
+ icon: css({
+ marginRight: theme.spacing(1),
+ }),
+});
+
export const Sparkline: React.FC = memo((props) => {
const { sparkline, config: fieldConfig, theme, width, height } = props;
const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, fieldConfig);
if (warning) {
- return null;
+ return {warning};
}
const data = preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame));
diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts
index fa07cafb604..43137d12e52 100644
--- a/packages/grafana-ui/src/components/Sparkline/utils.ts
+++ b/packages/grafana-ui/src/components/Sparkline/utils.ts
@@ -123,6 +123,15 @@ export const prepareSeries = (
frame,
};
}
+ if (sparkline.x && !isLikelyAscendingVector(sparkline.x.values)) {
+ return {
+ warning: t(
+ 'grafana-ui.components.sparkline.warning.x-not-ascending',
+ "The data in your Sparkline's x series must be sorted in ascending order."
+ ),
+ frame,
+ };
+ }
return { frame };
};
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 50a8504a38a..b2b57f26999 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -8695,8 +8695,12 @@
},
"components": {
"sparkline": {
+ "alert": {
+ "title": "Cannot render sparkline"
+ },
"warning": {
- "too-few-values": "Sparkline requires at least two values to render."
+ "too-few-values": "Sparkline requires at least two values to render.",
+ "x-not-ascending": "The data in your Sparkline's x series must be sorted in ascending order."
}
}
},