mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
XYChart further improvements (#55152)
* Tooltip shows all data facets. Renamed options * Add per series line style * Remove line style option from manual panel options * Refactored tooltip view * sets selected after switch to manual * remove facet prefixes * in manual mode pull series names from config options, not y facet * unused import * Point size * x & y axes labels * Fix manual series prep * betterer Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
parent
11eb02a183
commit
3361f2c62d
@ -8940,6 +8940,9 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
[0, 0, 0, "Do not use any type assertions.", "10"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
|
||||||
],
|
],
|
||||||
|
"public/app/plugins/panel/xychart/TooltipView.tsx:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
],
|
||||||
"public/app/plugins/panel/xychart/XYChartPanel2.tsx:5381": [
|
"public/app/plugins/panel/xychart/XYChartPanel2.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { FC, useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme, StandardEditorProps } from '@grafana/data';
|
import { GrafanaTheme, StandardEditorProps } from '@grafana/data';
|
||||||
import { Button, Field, IconButton, useStyles } from '@grafana/ui';
|
import { Button, Field, IconButton, useStyles } from '@grafana/ui';
|
||||||
@ -9,12 +9,12 @@ import { ColorDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensi
|
|||||||
|
|
||||||
import { XYChartOptions, ScatterSeriesConfig, defaultScatterConfig } from './models.gen';
|
import { XYChartOptions, ScatterSeriesConfig, defaultScatterConfig } from './models.gen';
|
||||||
|
|
||||||
export const ManualEditor: FC<StandardEditorProps<ScatterSeriesConfig[], any, XYChartOptions>> = ({
|
export const ManualEditor = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
context,
|
context,
|
||||||
}) => {
|
}: StandardEditorProps<ScatterSeriesConfig[], any, XYChartOptions>) => {
|
||||||
const [selected, setSelected] = useState<number>(-1);
|
const [selected, setSelected] = useState<number>(0);
|
||||||
const style = useStyles(getStyles);
|
const style = useStyles(getStyles);
|
||||||
|
|
||||||
const onFieldChange = (val: any | undefined, index: number, field: string) => {
|
const onFieldChange = (val: any | undefined, index: number, field: string) => {
|
||||||
@ -36,22 +36,27 @@ export const ManualEditor: FC<StandardEditorProps<ScatterSeriesConfig[], any, XY
|
|||||||
pointSize: defaultScatterConfig.pointSize,
|
pointSize: defaultScatterConfig.pointSize,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
setSelected(value.length);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Component-did-mount callback to check if a new series should be created
|
||||||
|
useEffect(() => {
|
||||||
|
if (!value?.length) {
|
||||||
|
createNewSeries(); // adds a new series
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onSeriesDelete = (index: number) => {
|
const onSeriesDelete = (index: number) => {
|
||||||
onChange(value.filter((_, i) => i !== index));
|
onChange(value.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const { options } = context;
|
// const { options } = context;
|
||||||
|
|
||||||
const getRowStyle = (index: number) => {
|
const getRowStyle = (index: number) => {
|
||||||
return index === selected ? `${style.row} ${style.sel}` : style.row;
|
return index === selected ? `${style.row} ${style.sel}` : style.row;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options === undefined || !options.series) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button icon="plus" size="sm" variant="secondary" onClick={createNewSeries} className={style.marginBot}>
|
<Button icon="plus" size="sm" variant="secondary" onClick={createNewSeries} className={style.marginBot}>
|
||||||
@ -59,7 +64,7 @@ export const ManualEditor: FC<StandardEditorProps<ScatterSeriesConfig[], any, XY
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className={style.marginBot}>
|
<div className={style.marginBot}>
|
||||||
{options.series.map((series, index) => {
|
{value.map((series, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={`series/${index}`} className={getRowStyle(index)} onMouseDown={() => setSelected(index)}>
|
<div key={`series/${index}`} className={getRowStyle(index)} onMouseDown={() => setSelected(index)}>
|
||||||
<LayerName
|
<LayerName
|
||||||
@ -78,12 +83,12 @@ export const ManualEditor: FC<StandardEditorProps<ScatterSeriesConfig[], any, XY
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selected >= 0 && options.series[selected] && (
|
{selected >= 0 && value[selected] && (
|
||||||
<>
|
<>
|
||||||
<div key={`series/${selected}`}>
|
<div key={`series/${selected}`}>
|
||||||
<Field label={'X Field'}>
|
<Field label={'X Field'}>
|
||||||
<FieldNamePicker
|
<FieldNamePicker
|
||||||
value={options.series[selected].x ?? ''}
|
value={value[selected].x ?? ''}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={(field) => onFieldChange(field, selected, 'x')}
|
onChange={(field) => onFieldChange(field, selected, 'x')}
|
||||||
item={{} as any}
|
item={{} as any}
|
||||||
@ -91,7 +96,7 @@ export const ManualEditor: FC<StandardEditorProps<ScatterSeriesConfig[], any, XY
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'Y Field'}>
|
<Field label={'Y Field'}>
|
||||||
<FieldNamePicker
|
<FieldNamePicker
|
||||||
value={options.series[selected].y ?? ''}
|
value={value[selected].y ?? ''}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={(field) => onFieldChange(field, selected, 'y')}
|
onChange={(field) => onFieldChange(field, selected, 'y')}
|
||||||
item={{} as any}
|
item={{} as any}
|
||||||
@ -99,7 +104,7 @@ export const ManualEditor: FC<StandardEditorProps<ScatterSeriesConfig[], any, XY
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'Point color'}>
|
<Field label={'Point color'}>
|
||||||
<ColorDimensionEditor
|
<ColorDimensionEditor
|
||||||
value={options.series[selected].pointColor!}
|
value={value[selected].pointColor!}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={(field) => onFieldChange(field, selected, 'pointColor')}
|
onChange={(field) => onFieldChange(field, selected, 'pointColor')}
|
||||||
item={{} as any}
|
item={{} as any}
|
||||||
@ -107,7 +112,7 @@ export const ManualEditor: FC<StandardEditorProps<ScatterSeriesConfig[], any, XY
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label={'Point size'}>
|
<Field label={'Point size'}>
|
||||||
<ScaleDimensionEditor
|
<ScaleDimensionEditor
|
||||||
value={options.series[selected].pointSize!}
|
value={value[selected].pointSize!}
|
||||||
context={context}
|
context={context}
|
||||||
onChange={(field) => onFieldChange(field, selected, 'pointSize')}
|
onChange={(field) => onFieldChange(field, selected, 'pointSize')}
|
||||||
item={{ settings: { min: 1, max: 50 } } as any}
|
item={{ settings: { min: 1, max: 50 } } as any}
|
||||||
|
@ -2,7 +2,6 @@ import { css } from '@emotion/css';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
arrayUtils,
|
|
||||||
DataFrame,
|
DataFrame,
|
||||||
Field,
|
Field,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
@ -11,30 +10,47 @@ import {
|
|||||||
LinkModel,
|
LinkModel,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { SortOrder } from '@grafana/schema';
|
import { LinkButton, usePanelContext, useStyles2, VerticalGroup, VizTooltipOptions } from '@grafana/ui';
|
||||||
import {
|
|
||||||
LinkButton,
|
|
||||||
SeriesIcon,
|
|
||||||
TooltipDisplayMode,
|
|
||||||
usePanelContext,
|
|
||||||
useStyles2,
|
|
||||||
VerticalGroup,
|
|
||||||
VizTooltipOptions,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||||
|
|
||||||
|
import { ScatterSeriesConfig, SeriesMapping } from './models.gen';
|
||||||
import { ScatterSeries } from './types';
|
import { ScatterSeries } from './types';
|
||||||
|
|
||||||
|
interface YValue {
|
||||||
|
name: string;
|
||||||
|
val: number;
|
||||||
|
field: Field;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExtraFacets {
|
||||||
|
colorFacetFieldName: string;
|
||||||
|
sizeFacetFieldName: string;
|
||||||
|
colorFacetValue: number;
|
||||||
|
sizeFacetValue: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
allSeries: ScatterSeries[];
|
allSeries: ScatterSeries[];
|
||||||
data: DataFrame[]; // source data
|
data: DataFrame[]; // source data
|
||||||
|
manualSeriesConfigs: ScatterSeriesConfig[] | undefined;
|
||||||
rowIndex?: number; // the hover row
|
rowIndex?: number; // the hover row
|
||||||
|
seriesMapping: SeriesMapping;
|
||||||
hoveredPointIndex: number; // the hovered point
|
hoveredPointIndex: number; // the hovered point
|
||||||
options: VizTooltipOptions;
|
options: VizTooltipOptions;
|
||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TooltipView = ({ allSeries, data, rowIndex, hoveredPointIndex, options, range }: Props) => {
|
export const TooltipView = ({
|
||||||
|
allSeries,
|
||||||
|
data,
|
||||||
|
manualSeriesConfigs,
|
||||||
|
seriesMapping,
|
||||||
|
rowIndex,
|
||||||
|
hoveredPointIndex,
|
||||||
|
options,
|
||||||
|
range,
|
||||||
|
}: Props) => {
|
||||||
const style = useStyles2(getStyles);
|
const style = useStyles2(getStyles);
|
||||||
const { onSplitOpen } = usePanelContext();
|
const { onSplitOpen } = usePanelContext();
|
||||||
|
|
||||||
@ -53,72 +69,57 @@ export const TooltipView = ({ allSeries, data, rowIndex, hoveredPointIndex, opti
|
|||||||
range,
|
range,
|
||||||
});
|
});
|
||||||
|
|
||||||
let yValues = [];
|
let yValue: YValue | null = null;
|
||||||
if (options.mode === TooltipDisplayMode.Single) {
|
let extraFacets: ExtraFacets | null = null;
|
||||||
yValues = [
|
if (seriesMapping === SeriesMapping.Manual && manualSeriesConfigs) {
|
||||||
{
|
const colorFacetFieldName = manualSeriesConfigs[hoveredPointIndex].pointColor?.field ?? '';
|
||||||
|
const sizeFacetFieldName = manualSeriesConfigs[hoveredPointIndex].pointSize?.field ?? '';
|
||||||
|
|
||||||
|
const colorFacet = colorFacetFieldName ? frame.fields.find((f) => f.name === colorFacetFieldName) : undefined;
|
||||||
|
const sizeFacet = sizeFacetFieldName ? frame.fields.find((f) => f.name === sizeFacetFieldName) : undefined;
|
||||||
|
|
||||||
|
extraFacets = {
|
||||||
|
colorFacetFieldName,
|
||||||
|
sizeFacetFieldName,
|
||||||
|
colorFacetValue: colorFacet?.values.get(rowIndex),
|
||||||
|
sizeFacetValue: sizeFacet?.values.get(rowIndex),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
yValue = {
|
||||||
name: getFieldDisplayName(yField, frame),
|
name: getFieldDisplayName(yField, frame),
|
||||||
val: yField.values.get(rowIndex),
|
val: yField.values.get(rowIndex),
|
||||||
field: yField,
|
field: yField,
|
||||||
color: series.pointColor(frame),
|
color: series.pointColor(frame) as string,
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
yValues = allSeries
|
|
||||||
.map((series, i) => {
|
|
||||||
const frame = series.frame(data);
|
|
||||||
const seriesXField = series.x(frame);
|
|
||||||
|
|
||||||
if (seriesXField.name !== xField.name) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const seriesYField = series.y(frame);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: getFieldDisplayName(seriesYField, frame),
|
|
||||||
val: seriesYField.values.get(rowIndex),
|
|
||||||
field: seriesYField,
|
|
||||||
color: allSeries[i].pointColor(frame),
|
|
||||||
};
|
};
|
||||||
})
|
|
||||||
.filter((v) => v != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.sort !== SortOrder.None) {
|
|
||||||
const sortFn = arrayUtils.sortValues(options.sort);
|
|
||||||
|
|
||||||
yValues.sort((a, b) => {
|
|
||||||
return sortFn(a!.val, b!.val);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let activePointIndex = -1;
|
|
||||||
activePointIndex = yValues.findIndex((v) => v!.name === series.name);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={style.xVal} aria-label="x-val">
|
|
||||||
{fmt(frame.fields[0], xField.values.get(rowIndex))}
|
|
||||||
</div>
|
|
||||||
<table className={style.infoWrap}>
|
<table className={style.infoWrap}>
|
||||||
<tbody>
|
<tr>
|
||||||
{yValues.map((el, index) => {
|
<th colSpan={2} style={{ backgroundColor: yValue.color }}></th>
|
||||||
let color = null;
|
|
||||||
if (typeof el!.color === 'string') {
|
|
||||||
color = el!.color;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={`${index}/${rowIndex}`} className={index === activePointIndex ? style.highlight : ''}>
|
|
||||||
<th>
|
|
||||||
{color && <SeriesIcon color={color} className={style.icon} />}
|
|
||||||
{el!.name}:
|
|
||||||
</th>
|
|
||||||
<td>{fmt(el!.field, el!.val)}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
<tbody>
|
||||||
})}
|
<tr>
|
||||||
|
<th>{xField.name}</th>
|
||||||
|
<td>{fmt(frame.fields[0], xField.values.get(rowIndex))}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{yValue.name}:</th>
|
||||||
|
<td>{fmt(yValue.field, yValue.val)}</td>
|
||||||
|
</tr>
|
||||||
|
{extraFacets !== null && extraFacets.colorFacetFieldName && (
|
||||||
|
<tr>
|
||||||
|
<th>{extraFacets.colorFacetFieldName}:</th>
|
||||||
|
<td>{extraFacets.colorFacetValue}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{extraFacets !== null && extraFacets.sizeFacetFieldName && (
|
||||||
|
<tr>
|
||||||
|
<th>{extraFacets.sizeFacetFieldName}:</th>
|
||||||
|
<td>{extraFacets.sizeFacetValue}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
{links.length > 0 && (
|
{links.length > 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={2}>
|
<td colSpan={2}>
|
||||||
|
@ -26,7 +26,7 @@ import { FacetedData } from '@grafana/ui/src/components/uPlot/types';
|
|||||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||||
|
|
||||||
import { TooltipView } from './TooltipView';
|
import { TooltipView } from './TooltipView';
|
||||||
import { XYChartOptions } from './models.gen';
|
import { SeriesMapping, XYChartOptions } from './models.gen';
|
||||||
import { prepData, prepScatter, ScatterPanelInfo } from './scatter';
|
import { prepData, prepScatter, ScatterPanelInfo } from './scatter';
|
||||||
import { ScatterHoverEvent, ScatterSeries } from './types';
|
import { ScatterHoverEvent, ScatterSeries } from './types';
|
||||||
|
|
||||||
@ -73,7 +73,9 @@ export const XYChartPanel2: React.FC<Props> = (props: Props) => {
|
|||||||
isToolTipOpen
|
isToolTipOpen
|
||||||
);
|
);
|
||||||
|
|
||||||
if (info.series.length && props.data.series) {
|
if (info.error) {
|
||||||
|
setError(info.error);
|
||||||
|
} else if (info.series.length && props.data.series) {
|
||||||
setBuilder(info.builder);
|
setBuilder(info.builder);
|
||||||
setSeries(info.series);
|
setSeries(info.series);
|
||||||
setFacets(() => prepData(info, props.data.series));
|
setFacets(() => prepData(info, props.data.series));
|
||||||
@ -99,10 +101,11 @@ export const XYChartPanel2: React.FC<Props> = (props: Props) => {
|
|||||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||||
const theme = config.theme2;
|
const theme = config.theme2;
|
||||||
|
|
||||||
for (const s of series) {
|
for (let si = 0; si < series.length; si++) {
|
||||||
|
const s = series[si];
|
||||||
const frame = s.frame(props.data.series);
|
const frame = s.frame(props.data.series);
|
||||||
if (frame) {
|
if (frame) {
|
||||||
for (const item of s.legend(frame)) {
|
for (const item of s.legend()) {
|
||||||
item.getDisplayValues = () => {
|
item.getDisplayValues = () => {
|
||||||
const calcs = props.options.legend.calcs;
|
const calcs = props.options.legend.calcs;
|
||||||
|
|
||||||
@ -167,6 +170,10 @@ export const XYChartPanel2: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
item.disabled = !(s.show ?? true);
|
item.disabled = !(s.show ?? true);
|
||||||
|
|
||||||
|
if (props.options.seriesMapping === SeriesMapping.Manual) {
|
||||||
|
item.label = props.options.series?.[si]?.name ?? `Series ${si + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,6 +247,8 @@ export const XYChartPanel2: React.FC<Props> = (props: Props) => {
|
|||||||
<TooltipView
|
<TooltipView
|
||||||
options={props.options.tooltip}
|
options={props.options.tooltip}
|
||||||
allSeries={series}
|
allSeries={series}
|
||||||
|
manualSeriesConfigs={props.options.series}
|
||||||
|
seriesMapping={props.options.seriesMapping!}
|
||||||
rowIndex={hover.xIndex}
|
rowIndex={hover.xIndex}
|
||||||
hoveredPointIndex={hover.scatterIndex}
|
hoveredPointIndex={hover.scatterIndex}
|
||||||
data={props.data.series}
|
data={props.data.series}
|
||||||
|
@ -5,12 +5,12 @@ import {
|
|||||||
identityOverrideProcessor,
|
identityOverrideProcessor,
|
||||||
SetFieldConfigOptionsArgs,
|
SetFieldConfigOptionsArgs,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { LineStyle, VisibilityMode } from '@grafana/schema';
|
import { LineStyle } from '@grafana/schema';
|
||||||
import { commonOptionsBuilder, graphFieldOptions } from '@grafana/ui';
|
import { commonOptionsBuilder } from '@grafana/ui';
|
||||||
|
|
||||||
import { LineStyleEditor } from '../timeseries/LineStyleEditor';
|
import { LineStyleEditor } from '../timeseries/LineStyleEditor';
|
||||||
|
|
||||||
import { ScatterFieldConfig, ScatterLineMode } from './models.gen';
|
import { ScatterFieldConfig, ScatterShow } from './models.gen';
|
||||||
|
|
||||||
export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOptionsArgs<ScatterFieldConfig> {
|
export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOptionsArgs<ScatterFieldConfig> {
|
||||||
return {
|
return {
|
||||||
@ -30,40 +30,33 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
|||||||
useCustomConfig: (builder) => {
|
useCustomConfig: (builder) => {
|
||||||
builder
|
builder
|
||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'point',
|
path: 'show',
|
||||||
name: 'Points',
|
name: 'Show',
|
||||||
defaultValue: cfg.point,
|
defaultValue: cfg.show,
|
||||||
settings: {
|
settings: {
|
||||||
options: graphFieldOptions.showPoints,
|
options: [
|
||||||
|
{ label: 'Points', value: ScatterShow.Points },
|
||||||
|
{ label: 'Lines', value: ScatterShow.Lines },
|
||||||
|
{ label: 'Both', value: ScatterShow.PointsAndLines },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
path: 'pointSize.fixed',
|
path: 'pointSize.fixed',
|
||||||
name: 'Size',
|
name: 'Point size',
|
||||||
defaultValue: cfg.pointSize?.fixed,
|
defaultValue: cfg.pointSize?.fixed,
|
||||||
settings: {
|
settings: {
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 100,
|
max: 100,
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
showIf: (c) => c.point !== VisibilityMode.Never,
|
showIf: (c) => c.show !== ScatterShow.Lines,
|
||||||
})
|
|
||||||
.addRadio({
|
|
||||||
path: 'line',
|
|
||||||
name: 'Lines',
|
|
||||||
defaultValue: cfg.line,
|
|
||||||
settings: {
|
|
||||||
options: [
|
|
||||||
{ label: 'None', value: ScatterLineMode.None },
|
|
||||||
{ label: 'Linear', value: ScatterLineMode.Linear },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.addCustomEditor<void, LineStyle>({
|
.addCustomEditor<void, LineStyle>({
|
||||||
id: 'lineStyle',
|
id: 'lineStyle',
|
||||||
path: 'lineStyle',
|
path: 'lineStyle',
|
||||||
name: 'Line style',
|
name: 'Line style',
|
||||||
showIf: (c) => c.line !== ScatterLineMode.None,
|
showIf: (c) => c.show !== ScatterShow.Points,
|
||||||
editor: LineStyleEditor,
|
editor: LineStyleEditor,
|
||||||
override: LineStyleEditor,
|
override: LineStyleEditor,
|
||||||
process: identityOverrideProcessor,
|
process: identityOverrideProcessor,
|
||||||
@ -78,7 +71,7 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
|||||||
max: 10,
|
max: 10,
|
||||||
step: 1,
|
step: 1,
|
||||||
},
|
},
|
||||||
showIf: (c) => c.line !== ScatterLineMode.None,
|
showIf: (c) => c.show !== ScatterShow.Points,
|
||||||
});
|
});
|
||||||
|
|
||||||
commonOptionsBuilder.addAxisConfig(builder, cfg);
|
commonOptionsBuilder.addAxisConfig(builder, cfg);
|
||||||
|
@ -14,20 +14,31 @@ import {
|
|||||||
TextDimensionConfig,
|
TextDimensionConfig,
|
||||||
} from 'app/features/dimensions';
|
} from 'app/features/dimensions';
|
||||||
|
|
||||||
export enum ScatterLineMode {
|
// export enum ScatterLineMode {
|
||||||
None = 'none',
|
// None = 'none',
|
||||||
Linear = 'linear',
|
// Linear = 'linear',
|
||||||
// Smooth
|
// Smooth
|
||||||
// r2, etc
|
// r2, etc
|
||||||
|
// }
|
||||||
|
|
||||||
|
export enum ScatterShow {
|
||||||
|
Points = 'points',
|
||||||
|
Lines = 'lines',
|
||||||
|
PointsAndLines = 'points+lines',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SeriesMapping {
|
||||||
|
Auto = 'auto',
|
||||||
|
Manual = 'manual',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScatterFieldConfig extends HideableFieldConfig, AxisConfig {
|
export interface ScatterFieldConfig extends HideableFieldConfig, AxisConfig {
|
||||||
line?: ScatterLineMode;
|
show?: ScatterShow;
|
||||||
|
|
||||||
lineWidth?: number;
|
lineWidth?: number;
|
||||||
lineStyle?: LineStyle;
|
lineStyle?: LineStyle;
|
||||||
lineColor?: ColorDimensionConfig;
|
lineColor?: ColorDimensionConfig;
|
||||||
|
|
||||||
point?: VisibilityMode;
|
|
||||||
pointSize?: ScaleDimensionConfig; // only 'fixed' is exposed in the UI
|
pointSize?: ScaleDimensionConfig; // only 'fixed' is exposed in the UI
|
||||||
pointColor?: ColorDimensionConfig;
|
pointColor?: ColorDimensionConfig;
|
||||||
pointSymbol?: DimensionSupplier<string>;
|
pointSymbol?: DimensionSupplier<string>;
|
||||||
@ -44,12 +55,11 @@ export interface ScatterSeriesConfig extends ScatterFieldConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const defaultScatterConfig: ScatterFieldConfig = {
|
export const defaultScatterConfig: ScatterFieldConfig = {
|
||||||
line: ScatterLineMode.None, // no line
|
show: ScatterShow.Points,
|
||||||
lineWidth: 1,
|
lineWidth: 1,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
fill: 'solid',
|
fill: 'solid',
|
||||||
},
|
},
|
||||||
point: VisibilityMode.Auto,
|
|
||||||
pointSize: {
|
pointSize: {
|
||||||
fixed: 5,
|
fixed: 5,
|
||||||
min: 1,
|
min: 1,
|
||||||
@ -66,7 +76,7 @@ export interface XYDimensionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface XYChartOptions extends OptionsWithLegend, OptionsWithTooltip {
|
export interface XYChartOptions extends OptionsWithLegend, OptionsWithTooltip {
|
||||||
mode?: 'auto' | 'manual';
|
seriesMapping?: SeriesMapping;
|
||||||
dims: XYDimensionConfig;
|
dims: XYDimensionConfig;
|
||||||
|
|
||||||
series?: ScatterSeriesConfig[];
|
series?: ScatterSeriesConfig[];
|
||||||
|
@ -12,8 +12,8 @@ export const plugin = new PanelPlugin<XYChartOptions, ScatterFieldConfig>(XYChar
|
|||||||
.setPanelOptions((builder) => {
|
.setPanelOptions((builder) => {
|
||||||
builder
|
builder
|
||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'mode',
|
path: 'seriesMapping',
|
||||||
name: 'Mode',
|
name: 'Series mapping',
|
||||||
defaultValue: 'auto',
|
defaultValue: 'auto',
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
@ -27,7 +27,7 @@ export const plugin = new PanelPlugin<XYChartOptions, ScatterFieldConfig>(XYChar
|
|||||||
path: 'dims',
|
path: 'dims',
|
||||||
name: '',
|
name: '',
|
||||||
editor: AutoEditor,
|
editor: AutoEditor,
|
||||||
showIf: (cfg) => cfg.mode === 'auto',
|
showIf: (cfg) => cfg.seriesMapping === 'auto',
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
id: 'series',
|
id: 'series',
|
||||||
@ -35,7 +35,7 @@ export const plugin = new PanelPlugin<XYChartOptions, ScatterFieldConfig>(XYChar
|
|||||||
name: '',
|
name: '',
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
editor: ManualEditor,
|
editor: ManualEditor,
|
||||||
showIf: (cfg) => cfg.mode === 'manual',
|
showIf: (cfg) => cfg.seriesMapping === 'manual',
|
||||||
});
|
});
|
||||||
|
|
||||||
commonOptionsBuilder.addTooltipOptions(builder);
|
commonOptionsBuilder.addTooltipOptions(builder);
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
import { pointWithin, Quadtree, Rect } from '../barchart/quadtree';
|
import { pointWithin, Quadtree, Rect } from '../barchart/quadtree';
|
||||||
|
|
||||||
import { isGraphable } from './dims';
|
import { isGraphable } from './dims';
|
||||||
import { defaultScatterConfig, ScatterFieldConfig, ScatterLineMode, XYChartOptions } from './models.gen';
|
import { defaultScatterConfig, ScatterFieldConfig, ScatterShow, XYChartOptions } from './models.gen';
|
||||||
import { DimensionValues, ScatterHoverCallback, ScatterSeries } from './types';
|
import { DimensionValues, ScatterHoverCallback, ScatterSeries } from './types';
|
||||||
|
|
||||||
export interface ScatterPanelInfo {
|
export interface ScatterPanelInfo {
|
||||||
@ -161,7 +161,7 @@ function getScatterSeries(
|
|||||||
|
|
||||||
x: (frame) => frame.fields[xIndex],
|
x: (frame) => frame.fields[xIndex],
|
||||||
y: (frame) => frame.fields[yIndex],
|
y: (frame) => frame.fields[yIndex],
|
||||||
legend: (frame) => {
|
legend: () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: name,
|
label: name,
|
||||||
@ -172,12 +172,12 @@ function getScatterSeries(
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
line: fieldConfig.line ?? ScatterLineMode.None,
|
showLine: fieldConfig.show !== ScatterShow.Points,
|
||||||
lineWidth: fieldConfig.lineWidth ?? 2,
|
lineWidth: fieldConfig.lineWidth ?? 2,
|
||||||
lineStyle: fieldConfig.lineStyle!,
|
lineStyle: fieldConfig.lineStyle!,
|
||||||
lineColor: () => seriesColor,
|
lineColor: () => seriesColor,
|
||||||
|
|
||||||
point: fieldConfig.point!,
|
showPoints: fieldConfig.show !== ScatterShow.Lines ? VisibilityMode.Always : VisibilityMode.Never,
|
||||||
pointSize,
|
pointSize,
|
||||||
pointColor,
|
pointColor,
|
||||||
pointSymbol: (frame: DataFrame, from?: number) => 'circle', // single field, multiple symbols.... kinda equals multiple series 🤔
|
pointSymbol: (frame: DataFrame, from?: number) => 'circle', // single field, multiple symbols.... kinda equals multiple series 🤔
|
||||||
@ -201,8 +201,11 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
|||||||
throw 'Missing data';
|
throw 'Missing data';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.mode === 'manual') {
|
if (options.seriesMapping === 'manual') {
|
||||||
if (options.series?.length) {
|
if (!options.series?.length) {
|
||||||
|
throw 'Missing series config';
|
||||||
|
}
|
||||||
|
|
||||||
const scatterSeries: ScatterSeries[] = [];
|
const scatterSeries: ScatterSeries[] = [];
|
||||||
|
|
||||||
for (const series of options.series) {
|
for (const series of options.series) {
|
||||||
@ -239,7 +242,6 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
|||||||
|
|
||||||
return scatterSeries;
|
return scatterSeries;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Default behavior
|
// Default behavior
|
||||||
const dims = options.dims ?? {};
|
const dims = options.dims ?? {};
|
||||||
@ -323,9 +325,9 @@ const prepConfig = (
|
|||||||
const scatterInfo = scatterSeries[seriesIdx - 1];
|
const scatterInfo = scatterSeries[seriesIdx - 1];
|
||||||
let d = u.data[seriesIdx] as unknown as FacetSeries;
|
let d = u.data[seriesIdx] as unknown as FacetSeries;
|
||||||
|
|
||||||
let showLine = scatterInfo.line !== ScatterLineMode.None;
|
let showLine = scatterInfo.showLine;
|
||||||
let showPoints = scatterInfo.point === VisibilityMode.Always;
|
let showPoints = scatterInfo.showPoints === VisibilityMode.Always;
|
||||||
if (!showPoints && scatterInfo.point === VisibilityMode.Auto) {
|
if (!showPoints && scatterInfo.showPoints === VisibilityMode.Auto) {
|
||||||
showPoints = d[0].length < 1000;
|
showPoints = d[0].length < 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,16 +591,22 @@ const prepConfig = (
|
|||||||
range: (u, min, max) => [min, max],
|
range: (u, min, max) => [min, max],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// why does this fall back to '' instead of null or undef?
|
||||||
|
let xAxisLabel = xField.config.custom.axisLabel;
|
||||||
|
|
||||||
builder.addAxis({
|
builder.addAxis({
|
||||||
scaleKey: 'x',
|
scaleKey: 'x',
|
||||||
placement:
|
placement:
|
||||||
xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden,
|
xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden,
|
||||||
show: xField.config.custom?.axisPlacement !== AxisPlacement.Hidden,
|
show: xField.config.custom?.axisPlacement !== AxisPlacement.Hidden,
|
||||||
theme,
|
theme,
|
||||||
label: xField.config.custom.axisLabel,
|
label:
|
||||||
|
xAxisLabel == null || xAxisLabel === ''
|
||||||
|
? getFieldDisplayName(xField, scatterSeries[0].frame(frames), frames)
|
||||||
|
: xAxisLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
scatterSeries.forEach((s) => {
|
scatterSeries.forEach((s, si) => {
|
||||||
let frame = s.frame(frames);
|
let frame = s.frame(frames);
|
||||||
let field = s.y(frame);
|
let field = s.y(frame);
|
||||||
|
|
||||||
@ -617,11 +625,17 @@ const prepConfig = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (field.config.custom?.axisPlacement !== AxisPlacement.Hidden) {
|
if (field.config.custom?.axisPlacement !== AxisPlacement.Hidden) {
|
||||||
|
// why does this fall back to '' instead of null or undef?
|
||||||
|
let yAxisLabel = field.config.custom?.axisLabel;
|
||||||
|
|
||||||
builder.addAxis({
|
builder.addAxis({
|
||||||
scaleKey,
|
scaleKey,
|
||||||
theme,
|
theme,
|
||||||
placement: field.config.custom?.axisPlacement,
|
placement: field.config.custom?.axisPlacement,
|
||||||
label: field.config.custom.axisLabel,
|
label:
|
||||||
|
yAxisLabel == null || yAxisLabel === ''
|
||||||
|
? getFieldDisplayName(field, scatterSeries[si].frame(frames), frames)
|
||||||
|
: yAxisLabel,
|
||||||
values: (u, splits) => splits.map((s) => field.display!(s).text),
|
values: (u, splits) => splits.map((s) => field.display!(s).text),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ import { LineStyle, VisibilityMode } from '@grafana/schema';
|
|||||||
import { VizLegendItem } from '@grafana/ui';
|
import { VizLegendItem } from '@grafana/ui';
|
||||||
import { ScaleDimensionConfig } from 'app/features/dimensions';
|
import { ScaleDimensionConfig } from 'app/features/dimensions';
|
||||||
|
|
||||||
import { ScatterLineMode } from './models.gen';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -37,14 +35,14 @@ export interface ScatterSeries {
|
|||||||
x: (frame: DataFrame) => Field;
|
x: (frame: DataFrame) => Field;
|
||||||
y: (frame: DataFrame) => Field;
|
y: (frame: DataFrame) => Field;
|
||||||
|
|
||||||
legend: (frame: DataFrame) => VizLegendItem[]; // could be single if symbol is constant
|
legend: () => VizLegendItem[]; // could be single if symbol is constant
|
||||||
|
|
||||||
line: ScatterLineMode;
|
showLine: boolean;
|
||||||
lineWidth: number;
|
lineWidth: number;
|
||||||
lineStyle: LineStyle;
|
lineStyle: LineStyle;
|
||||||
lineColor: (frame: DataFrame) => CanvasRenderingContext2D['strokeStyle'];
|
lineColor: (frame: DataFrame) => CanvasRenderingContext2D['strokeStyle'];
|
||||||
|
|
||||||
point: VisibilityMode;
|
showPoints: VisibilityMode;
|
||||||
pointSize: DimensionValues<number>;
|
pointSize: DimensionValues<number>;
|
||||||
pointColor: DimensionValues<CanvasRenderingContext2D['strokeStyle']>;
|
pointColor: DimensionValues<CanvasRenderingContext2D['strokeStyle']>;
|
||||||
pointSymbol: DimensionValues<string>; // single field, multiple symbols.... kinda equals multiple series
|
pointSymbol: DimensionValues<string>; // single field, multiple symbols.... kinda equals multiple series
|
||||||
|
Loading…
Reference in New Issue
Block a user