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:
Victor Marin 2022-09-27 18:18:42 +03:00 committed by GitHub
parent 11eb02a183
commit 3361f2c62d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 205 additions and 172 deletions

View File

@ -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"]
], ],

View File

@ -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}

View File

@ -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}>

View File

@ -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}

View File

@ -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);

View File

@ -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[];

View File

@ -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);

View File

@ -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),
}); });
} }

View File

@ -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