XYChart: Add support for x=time (#106459)

* XYChart: Add support for x=time

* prettier

* Add time axis demo to provisioned dashboard for XY

* Add details about time fields to the documentation

---------

Co-authored-by: Kristina Durivage <kristina.durivage@grafana.com>
pull/106982/head^2
Leon Sorokin 7 months ago committed by GitHub
parent 17952d45e4
commit cf8e3bf7d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2509
      devenv/dev-dashboards/panel-xychart/xychart-demo.json
  2. 6
      docs/sources/panels-visualizations/visualizations/xy-chart/index.md
  3. 4
      packages/grafana-ui/src/components/uPlot/config/UPlotScaleBuilder.ts
  4. 5
      public/app/plugins/panel/xychart/SeriesEditor.tsx
  5. 8
      public/app/plugins/panel/xychart/scatter.ts
  6. 15
      public/app/plugins/panel/xychart/utils.ts

File diff suppressed because one or more lines are too long

@ -65,7 +65,7 @@ XY charts provide a way to visualize arbitrary x and y values in a graph so that
## Supported data formats
You can use any type of tabular data with at least two numeric fields in an xy chart. This type of visualization doesn't require time data.
You can use any type of tabular data with at least two numeric fields in an xy chart. The x field can be a time field. This type of visualization doesn't require time data, but it can be used.
## Configuration options
@ -117,7 +117,7 @@ When you select **Auto** as your series mapping mode, the following options are
| Option | Description |
| ------ | ----------- |
| Frame | By default, an xy chart displays all data frames. You can filter to select only one frame. |
| [X field](#x-field) | Select which field or fields x represents. By default, this is the first number field in each data frame. For an example of this in **Auto** mode, refer to the [X field section](#x-field). |
| [X field](#x-field) | Select which field or fields x represents. By default, this is the first number or time field in each data frame. For an example of this in **Auto** mode, refer to the [X field section](#x-field). |
| [Y field](#y-field) | After the x-field is set, by default, all the remaining number fields in the data frame are designated as the y-fields. You can use this option to explicitly choose which fields to use for y. For more information on how to use this in **Auto** mode, refer to the [Y field section](#y-field). |
<!-- prettier-ignore-end -->
@ -145,7 +145,7 @@ In **Manual** mode, these fields are required:
#### X field
In **Auto** series mapping mode, select which field or fields x represents. By default, this is the first number field in each data frame. For example, you enter the following CSV content:
In **Auto** series mapping mode, select which field or fields x represents. By default, this is the first number or time field in each data frame. For example, you enter the following CSV content:
| a | b | c |
| --- | --- | --- |

@ -8,6 +8,7 @@ import { PlotConfigBuilder } from '../types';
export interface ScaleProps {
scaleKey: string;
isTime?: boolean;
auto?: boolean;
min?: number | null;
max?: number | null;
softMin?: number | null;
@ -32,6 +33,7 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
getConfig(): Scale {
let {
isTime,
auto,
scaleKey,
min: hardMin,
max: hardMax,
@ -256,7 +258,7 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
return minMax;
};
let auto = !isTime && !hasFixedRange;
auto ??= !isTime && !hasFixedRange;
if (isBooleanUnit(scaleKey)) {
auto = false;

@ -182,10 +182,11 @@ export const SeriesEditor = ({
filter: (field) =>
(mapping === SeriesMapping.Auto ||
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
field.type === FieldType.number &&
(field.type === FieldType.number || field.type === FieldType.time) &&
!field.config.custom?.hideFrom?.viz,
baseNameMode,
placeholderText: mapping === SeriesMapping.Auto ? 'First number field in each frame' : undefined,
placeholderText:
mapping === SeriesMapping.Auto ? 'First number or time field in each frame' : undefined,
},
}}
/>

@ -298,6 +298,7 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
builder.setMode(2);
let xField = xySeries[0].x.field;
let xIsTime = xField.type === FieldType.time;
let fieldConfig = xField.config;
let customConfig = fieldConfig.custom;
@ -305,7 +306,8 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
builder.addScale({
scaleKey: 'x',
isTime: false,
isTime: xIsTime,
auto: true,
orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right,
distribution: scaleDistr?.type,
@ -317,6 +319,7 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
softMax: customConfig?.axisSoftMax,
centeredZero: customConfig?.axisCenteredZero,
decimals: fieldConfig.decimals,
range: xIsTime ? (u, min, max) => [min, max] : undefined,
});
// why does this fall back to '' instead of null or undef?
@ -339,13 +342,14 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
builder.addAxis({
scaleKey: 'x',
isTime: xIsTime,
placement: customConfig?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden,
show: customConfig?.axisPlacement !== AxisPlacement.Hidden,
grid: { show: customConfig?.axisGridShow },
border: { show: customConfig?.axisBorderShow },
theme,
label: xAxisLabel,
formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)),
formatValue: xIsTime ? undefined : (v, decimals) => formattedValueToString(xField.display!(v, decimals)),
});
xySeries.forEach((s, si) => {

@ -62,8 +62,8 @@ export function prepSeries(
let xMatcher = getFieldMatcher(
seriesCfg.x?.matcher ?? {
id: FieldMatcherID.byType,
options: 'number',
id: FieldMatcherID.byTypes,
options: new Set(['number', 'time']),
}
);
let yMatcher = getFieldMatcher(
@ -89,11 +89,16 @@ export function prepSeries(
let frameSeries: XYSeries[] = [];
// only grabbing number fields (exclude time, string, enum, other)
let onlyNumFields = frame.fields.filter((field) => field.type === FieldType.number);
let onlyNumTimeFields = frame.fields.filter(
(field) => field.type === FieldType.number || field.type === FieldType.time
);
// only one of these per frame
let x = onlyNumFields.find((field) => xMatcher(field, frame, frames));
let x = onlyNumTimeFields.find((field) => xMatcher(field, frame, frames));
// only grabbing number fields (exclude time, string, enum, other)
let onlyNumFields = onlyNumTimeFields.filter((field) => field.type === FieldType.number);
let color =
colorMatcher != null
? onlyNumFields.find((field) => field !== x && colorMatcher!(field, frame, frames))

Loading…
Cancel
Save