mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
XYPanel: Improvements (#54220)
* Fix panel option bugs and make tooltip options work * Support multiple series in explicit mode. Rename XY/Explicit to Auto/Manual * Fixes * Fix * Legend improvements * Rewrite Pure Component to Function Component * Add datalinks support * Legend fixes and CR modifications * Fix bugs that crash panel
This commit is contained in:
parent
4952b7f22d
commit
6d2352735d
@ -9253,10 +9253,24 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/xychart/ExplicitEditor.tsx:5381": [
|
||||
"public/app/plugins/panel/xychart/AutoEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/xychart/XYDimsEditor.tsx:5381": [
|
||||
"public/app/plugins/panel/xychart/ManualEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "8"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
|
||||
],
|
||||
"public/app/plugins/panel/xychart/XYChartPanel2.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/xychart/dims.ts:5381": [
|
||||
|
@ -234,7 +234,6 @@ export class TestDataDataSource extends DataSourceWithBackend<TestDataQuery> {
|
||||
options: DataQueryRequest<TestDataQuery>
|
||||
): Observable<DataQueryResponse> | null {
|
||||
const { errorType } = target;
|
||||
console.log("we're here!", target);
|
||||
|
||||
if (errorType === 'server_panic') {
|
||||
return null;
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
getFieldDisplayName,
|
||||
GrafanaTheme2,
|
||||
} from '@grafana/data';
|
||||
import { IconButton, Label, Select, useStyles2 } from '@grafana/ui';
|
||||
import { Field, IconButton, Select, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { getXYDimensions, isGraphable } from './dims';
|
||||
import { XYDimensionConfig, XYChartOptions } from './models.gen';
|
||||
@ -19,7 +19,7 @@ interface XYInfo {
|
||||
yFields: Array<SelectableValue<boolean>>;
|
||||
}
|
||||
|
||||
export const XYDimsEditor: FC<StandardEditorProps<XYDimensionConfig, any, XYChartOptions>> = ({
|
||||
export const AutoEditor: FC<StandardEditorProps<XYDimensionConfig, any, XYChartOptions>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
@ -89,54 +89,55 @@ export const XYDimsEditor: FC<StandardEditorProps<XYDimensionConfig, any, XYChar
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
options={frameNames}
|
||||
value={frameNames.find((v) => v.value === value?.frame) ?? frameNames[0]}
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
...value,
|
||||
frame: v.value!,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<Label>X Field</Label>
|
||||
<Select
|
||||
options={info.numberFields}
|
||||
value={info.xAxis}
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
...value,
|
||||
x: v.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<br />
|
||||
<Label>Y Fields</Label>
|
||||
<div>
|
||||
{info.yFields.map((v) => (
|
||||
<div key={v.label} className={styles.row}>
|
||||
<IconButton
|
||||
name={v.value ? 'eye-slash' : 'eye'}
|
||||
onClick={() => {
|
||||
const exclude: string[] = value?.exclude ? [...value.exclude] : [];
|
||||
let idx = exclude.indexOf(v.label!);
|
||||
if (idx < 0) {
|
||||
exclude.push(v.label!);
|
||||
} else {
|
||||
exclude.splice(idx, 1);
|
||||
}
|
||||
onChange({
|
||||
...value,
|
||||
exclude,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{v.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<br /> <br />
|
||||
<Field label={'Data'}>
|
||||
<Select
|
||||
options={frameNames}
|
||||
value={frameNames.find((v) => v.value === value?.frame) ?? frameNames[0]}
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
...value,
|
||||
frame: v.value!,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'X Field'}>
|
||||
<Select
|
||||
options={info.numberFields}
|
||||
value={info.xAxis}
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
...value,
|
||||
x: v.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Y Fields'}>
|
||||
<div>
|
||||
{info.yFields.map((v) => (
|
||||
<div key={v.label} className={styles.row}>
|
||||
<IconButton
|
||||
name={v.value ? 'eye-slash' : 'eye'}
|
||||
onClick={() => {
|
||||
const exclude: string[] = value?.exclude ? [...value.exclude] : [];
|
||||
let idx = exclude.indexOf(v.label!);
|
||||
if (idx < 0) {
|
||||
exclude.push(v.label!);
|
||||
} else {
|
||||
exclude.splice(idx, 1);
|
||||
}
|
||||
onChange({
|
||||
...value,
|
||||
exclude,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{v.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
|
||||
import { XYChartOptions, ScatterFieldConfig } from './models.gen';
|
||||
|
||||
export const ExplicitEditor: FC<StandardEditorProps<ScatterFieldConfig[], any, XYChartOptions>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
return <div>TODO: explicit scatter config</div>;
|
||||
};
|
155
public/app/plugins/panel/xychart/ManualEditor.tsx
Normal file
155
public/app/plugins/panel/xychart/ManualEditor.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { FC, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme, StandardEditorProps } from '@grafana/data';
|
||||
import { Button, Field, IconButton, useStyles } from '@grafana/ui';
|
||||
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
||||
import { LayerName } from 'app/core/components/Layers/LayerName';
|
||||
import { ColorDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors';
|
||||
|
||||
import { XYChartOptions, ScatterSeriesConfig, defaultScatterConfig } from './models.gen';
|
||||
|
||||
export const ManualEditor: FC<StandardEditorProps<ScatterSeriesConfig[], any, XYChartOptions>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
const [selected, setSelected] = useState<number>(-1);
|
||||
const style = useStyles(getStyles);
|
||||
|
||||
const onFieldChange = (val: any | undefined, index: number, field: string) => {
|
||||
onChange(
|
||||
value.map((obj, i) => {
|
||||
if (i === index) {
|
||||
return { ...obj, [field]: val };
|
||||
}
|
||||
return obj;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const createNewSeries = () => {
|
||||
onChange([
|
||||
...value,
|
||||
{
|
||||
pointColor: {} as any,
|
||||
pointSize: defaultScatterConfig.pointSize,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onSeriesDelete = (index: number) => {
|
||||
onChange(value.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const { options } = context;
|
||||
|
||||
const getRowStyle = (index: number) => {
|
||||
return index === selected ? `${style.row} ${style.sel}` : style.row;
|
||||
};
|
||||
|
||||
if (options === undefined || !options.series) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button icon="plus" size="sm" variant="secondary" onClick={createNewSeries} className={style.marginBot}>
|
||||
Add series
|
||||
</Button>
|
||||
|
||||
<div className={style.marginBot}>
|
||||
{options.series.map((series, index) => {
|
||||
return (
|
||||
<div key={`series/${index}`} className={getRowStyle(index)} onMouseDown={() => setSelected(index)}>
|
||||
<LayerName
|
||||
name={series.name ?? `Series ${index + 1}`}
|
||||
onChange={(v) => onFieldChange(v, index, 'name')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
title={'remove'}
|
||||
className={cx(style.actionIcon)}
|
||||
onClick={() => onSeriesDelete(index)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{selected >= 0 && options.series[selected] && (
|
||||
<>
|
||||
<div key={`series/${selected}`}>
|
||||
<Field label={'X Field'}>
|
||||
<FieldNamePicker
|
||||
value={options.series[selected].x ?? ''}
|
||||
context={context}
|
||||
onChange={(field) => onFieldChange(field, selected, 'x')}
|
||||
item={{} as any}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Y Field'}>
|
||||
<FieldNamePicker
|
||||
value={options.series[selected].y ?? ''}
|
||||
context={context}
|
||||
onChange={(field) => onFieldChange(field, selected, 'y')}
|
||||
item={{} as any}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Point color'}>
|
||||
<ColorDimensionEditor
|
||||
value={options.series[selected].pointColor!}
|
||||
context={context}
|
||||
onChange={(field) => onFieldChange(field, selected, 'pointColor')}
|
||||
item={{} as any}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Point size'}>
|
||||
<ScaleDimensionEditor
|
||||
value={options.series[selected].pointSize!}
|
||||
context={context}
|
||||
onChange={(field) => onFieldChange(field, selected, 'pointSize')}
|
||||
item={{ settings: { min: 1, max: 50 } } as any}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
marginBot: css`
|
||||
margin-bottom: 20px;
|
||||
`,
|
||||
row: css`
|
||||
padding: ${theme.spacing.xs} ${theme.spacing.sm};
|
||||
border-radius: ${theme.border.radius.sm};
|
||||
background: ${theme.colors.bg2};
|
||||
min-height: ${theme.spacing.formInputHeight}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
border: 1px solid ${theme.colors.formInputBorder};
|
||||
&:hover {
|
||||
border: 1px solid ${theme.colors.formInputBorderHover};
|
||||
}
|
||||
`,
|
||||
sel: css`
|
||||
border: 1px solid ${theme.colors.formInputBorderActive};
|
||||
&:hover {
|
||||
border: 1px solid ${theme.colors.formInputBorderActive};
|
||||
}
|
||||
`,
|
||||
actionIcon: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
&:hover {
|
||||
color: ${theme.colors.text};
|
||||
}
|
||||
`,
|
||||
});
|
@ -1,55 +1,162 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { DataFrame, Field, formattedValueToString, getFieldDisplayName, GrafanaTheme2 } from '@grafana/data';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import {
|
||||
arrayUtils,
|
||||
DataFrame,
|
||||
Field,
|
||||
formattedValueToString,
|
||||
getFieldDisplayName,
|
||||
GrafanaTheme2,
|
||||
LinkModel,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { SortOrder } from '@grafana/schema';
|
||||
import {
|
||||
LinkButton,
|
||||
SeriesIcon,
|
||||
TooltipDisplayMode,
|
||||
usePanelContext,
|
||||
useStyles2,
|
||||
VerticalGroup,
|
||||
VizTooltipOptions,
|
||||
} from '@grafana/ui';
|
||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||
|
||||
import { ScatterSeries } from './types';
|
||||
|
||||
export interface Props {
|
||||
series: ScatterSeries;
|
||||
allSeries: ScatterSeries[];
|
||||
data: DataFrame[]; // source data
|
||||
rowIndex?: number; // the hover row
|
||||
hoveredPointIndex: number; // the hovered point
|
||||
options: VizTooltipOptions;
|
||||
range: TimeRange;
|
||||
}
|
||||
|
||||
export class TooltipView extends PureComponent<Props> {
|
||||
style = getStyles(config.theme2);
|
||||
export const TooltipView = ({ allSeries, data, rowIndex, hoveredPointIndex, options, range }: Props) => {
|
||||
const style = useStyles2(getStyles);
|
||||
const { onSplitOpen } = usePanelContext();
|
||||
|
||||
render() {
|
||||
const { series, data, rowIndex } = this.props;
|
||||
if (!series || rowIndex == null) {
|
||||
return null;
|
||||
}
|
||||
const frame = series.frame(data);
|
||||
const y = undefined; // series.y(frame);
|
||||
if (!allSeries || rowIndex == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={this.style.infoWrap}>
|
||||
const series = allSeries[hoveredPointIndex];
|
||||
const frame = series.frame(data);
|
||||
const xField = series.x(frame);
|
||||
const yField = series.y(frame);
|
||||
const links: Array<LinkModel<Field>> = getFieldLinksForExplore({
|
||||
field: yField,
|
||||
splitOpenFn: onSplitOpen,
|
||||
rowIndex,
|
||||
range,
|
||||
});
|
||||
|
||||
let yValues = [];
|
||||
if (options.mode === TooltipDisplayMode.Single) {
|
||||
yValues = [
|
||||
{
|
||||
name: getFieldDisplayName(yField, frame),
|
||||
val: yField.values.get(rowIndex),
|
||||
field: yField,
|
||||
color: series.pointColor(frame),
|
||||
},
|
||||
];
|
||||
} 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 (
|
||||
<>
|
||||
<div className={style.xVal} aria-label="x-val">
|
||||
{fmt(frame.fields[0], xField.values.get(rowIndex))}
|
||||
</div>
|
||||
<table className={style.infoWrap}>
|
||||
<tbody>
|
||||
{frame.fields.map((f, i) => (
|
||||
<tr key={`${i}/${rowIndex}`} className={f === y ? this.style.highlight : ''}>
|
||||
<th>{getFieldDisplayName(f, frame)}:</th>
|
||||
<td>{fmt(f, rowIndex)}</td>
|
||||
{yValues.map((el, index) => {
|
||||
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>
|
||||
);
|
||||
})}
|
||||
{links.length > 0 && (
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<VerticalGroup>
|
||||
{links.map((link, i) => (
|
||||
<LinkButton
|
||||
key={i}
|
||||
icon={'external-link-alt'}
|
||||
target={link.target}
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
fill="text"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{link.title}
|
||||
</LinkButton>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function fmt(field: Field, row: number): string {
|
||||
const v = field.values.get(row);
|
||||
function fmt(field: Field, val: number): string {
|
||||
if (field.display) {
|
||||
return formattedValueToString(field.display(v));
|
||||
return formattedValueToString(field.display(val));
|
||||
}
|
||||
return `${v}`;
|
||||
return `${val}`;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
infoWrap: css`
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
th {
|
||||
font-weight: ${theme.typography.fontWeightMedium};
|
||||
padding: ${theme.spacing(0.25, 2)};
|
||||
@ -58,4 +165,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
highlight: css`
|
||||
background: ${theme.colors.action.hover};
|
||||
`,
|
||||
}));
|
||||
xVal: css`
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
`,
|
||||
icon: css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
vertical-align: middle;
|
||||
`,
|
||||
});
|
||||
|
@ -1,10 +1,20 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { usePrevious } from 'react-use';
|
||||
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import {
|
||||
DisplayProcessor,
|
||||
DisplayValue,
|
||||
fieldReducers,
|
||||
PanelProps,
|
||||
reduceField,
|
||||
ReducerID,
|
||||
getDisplayProcessor,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
LegendDisplayMode,
|
||||
Portal,
|
||||
TooltipDisplayMode,
|
||||
UPlotChart,
|
||||
UPlotConfigBuilder,
|
||||
VizLayout,
|
||||
@ -13,116 +23,231 @@ import {
|
||||
VizTooltipContainer,
|
||||
} from '@grafana/ui';
|
||||
import { FacetedData } from '@grafana/ui/src/components/uPlot/types';
|
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||
|
||||
import { TooltipView } from './TooltipView';
|
||||
import { XYChartOptions } from './models.gen';
|
||||
import { prepData, prepScatter } from './scatter';
|
||||
import { prepData, prepScatter, ScatterPanelInfo } from './scatter';
|
||||
import { ScatterHoverEvent, ScatterSeries } from './types';
|
||||
|
||||
type Props = PanelProps<XYChartOptions>;
|
||||
type State = {
|
||||
error?: string;
|
||||
series: ScatterSeries[];
|
||||
builder?: UPlotConfigBuilder;
|
||||
facets?: FacetedData;
|
||||
hover?: ScatterHoverEvent;
|
||||
};
|
||||
const TOOLTIP_OFFSET = 10;
|
||||
|
||||
export class XYChartPanel2 extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
series: [],
|
||||
export const XYChartPanel2: React.FC<Props> = (props: Props) => {
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const [series, setSeries] = useState<ScatterSeries[]>([]);
|
||||
const [builder, setBuilder] = useState<UPlotConfigBuilder | undefined>();
|
||||
const [facets, setFacets] = useState<FacetedData | undefined>();
|
||||
const [hover, setHover] = useState<ScatterHoverEvent | undefined>();
|
||||
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
|
||||
|
||||
const isToolTipOpen = useRef<boolean>(false);
|
||||
const oldOptions = usePrevious(props.options);
|
||||
const oldData = usePrevious(props.data);
|
||||
|
||||
const onCloseToolTip = () => {
|
||||
isToolTipOpen.current = false;
|
||||
setShouldDisplayCloseButton(false);
|
||||
scatterHoverCallback(undefined);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.initSeries(); // also data
|
||||
}
|
||||
const onUPlotClick = () => {
|
||||
isToolTipOpen.current = !isToolTipOpen.current;
|
||||
|
||||
componentDidUpdate(oldProps: Props) {
|
||||
const { options, data } = this.props;
|
||||
const configsChanged = options !== oldProps.options || data.structureRev !== oldProps.data.structureRev;
|
||||
// Linking into useState required to re-render tooltip
|
||||
setShouldDisplayCloseButton(isToolTipOpen.current);
|
||||
};
|
||||
|
||||
if (configsChanged) {
|
||||
this.initSeries();
|
||||
} else if (data !== oldProps.data) {
|
||||
this.initFacets();
|
||||
const scatterHoverCallback = (hover?: ScatterHoverEvent) => {
|
||||
setHover(hover);
|
||||
};
|
||||
|
||||
const initSeries = useCallback(() => {
|
||||
const getData = () => props.data.series;
|
||||
const info: ScatterPanelInfo = prepScatter(
|
||||
props.options,
|
||||
getData,
|
||||
config.theme2,
|
||||
scatterHoverCallback,
|
||||
onUPlotClick,
|
||||
isToolTipOpen
|
||||
);
|
||||
|
||||
if (info.series.length && props.data.series) {
|
||||
setBuilder(info.builder);
|
||||
setSeries(info.series);
|
||||
setFacets(() => prepData(info, props.data.series));
|
||||
setError(undefined);
|
||||
}
|
||||
}
|
||||
}, [props.data.series, props.options]);
|
||||
|
||||
scatterHoverCallback = (hover?: ScatterHoverEvent) => {
|
||||
this.setState({ hover });
|
||||
};
|
||||
const initFacets = useCallback(() => {
|
||||
setFacets(() => prepData({ error, series }, props.data.series));
|
||||
}, [props.data.series, error, series]);
|
||||
|
||||
getData = () => {
|
||||
return this.props.data.series;
|
||||
};
|
||||
|
||||
initSeries = () => {
|
||||
const { options, data } = this.props;
|
||||
const info: State = prepScatter(options, this.getData, config.theme2, this.scatterHoverCallback);
|
||||
if (info.series.length && data.series) {
|
||||
info.facets = prepData(info, data.series);
|
||||
info.error = undefined;
|
||||
useEffect(() => {
|
||||
if (oldOptions !== props.options || oldData?.structureRev !== props.data.structureRev) {
|
||||
initSeries();
|
||||
} else if (oldData !== props.data) {
|
||||
initFacets();
|
||||
}
|
||||
this.setState(info);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props]);
|
||||
|
||||
initFacets = () => {
|
||||
this.setState({
|
||||
facets: prepData(this.state, this.props.data.series),
|
||||
});
|
||||
};
|
||||
|
||||
renderLegend = () => {
|
||||
const { data } = this.props;
|
||||
const { series } = this.state;
|
||||
const renderLegend = () => {
|
||||
const items: VizLegendItem[] = [];
|
||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||
const theme = config.theme2;
|
||||
|
||||
for (const s of series) {
|
||||
const frame = s.frame(data.series);
|
||||
const frame = s.frame(props.data.series);
|
||||
if (frame) {
|
||||
for (const item of s.legend(frame)) {
|
||||
item.getDisplayValues = () => {
|
||||
const calcs = props.options.legend.calcs;
|
||||
|
||||
if (!calcs?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const field = s.y(frame);
|
||||
|
||||
const fmt = field.display ?? defaultFormatter;
|
||||
let countFormatter: DisplayProcessor | null = null;
|
||||
|
||||
const fieldCalcs = reduceField({
|
||||
field,
|
||||
reducers: calcs,
|
||||
});
|
||||
|
||||
return calcs.map<DisplayValue>((reducerId) => {
|
||||
const fieldReducer = fieldReducers.get(reducerId);
|
||||
let formatter = fmt;
|
||||
|
||||
if (fieldReducer.id === ReducerID.diffperc) {
|
||||
formatter = getDisplayProcessor({
|
||||
field: {
|
||||
...field,
|
||||
config: {
|
||||
...field.config,
|
||||
unit: 'percent',
|
||||
},
|
||||
},
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
fieldReducer.id === ReducerID.count ||
|
||||
fieldReducer.id === ReducerID.changeCount ||
|
||||
fieldReducer.id === ReducerID.distinctCount
|
||||
) {
|
||||
if (!countFormatter) {
|
||||
countFormatter = getDisplayProcessor({
|
||||
field: {
|
||||
...field,
|
||||
config: {
|
||||
...field.config,
|
||||
unit: 'none',
|
||||
},
|
||||
},
|
||||
theme,
|
||||
});
|
||||
}
|
||||
formatter = countFormatter;
|
||||
}
|
||||
|
||||
return {
|
||||
...formatter(fieldCalcs[reducerId]),
|
||||
title: fieldReducer.name,
|
||||
description: fieldReducer.description,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
item.disabled = !(s.show ?? true);
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.options.legend.showLegend) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const legendStyle = {
|
||||
flexStart: css`
|
||||
div {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
return (
|
||||
<VizLayout.Legend placement="bottom">
|
||||
<VizLegend placement="bottom" items={items} displayMode={LegendDisplayMode.List} />
|
||||
<VizLayout.Legend placement={props.options.legend.placement} width={props.options.legend.width}>
|
||||
<VizLegend
|
||||
className={legendStyle.flexStart}
|
||||
placement={props.options.legend.placement}
|
||||
items={items}
|
||||
displayMode={props.options.legend.displayMode}
|
||||
/>
|
||||
</VizLayout.Legend>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { width, height, timeRange, data } = this.props;
|
||||
const { error, facets, builder, hover, series } = this.state;
|
||||
if (error || !builder) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !builder || !facets) {
|
||||
return (
|
||||
<>
|
||||
<VizLayout width={width} height={height} legend={this.renderLegend()}>
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
// <pre style={{ width: vizWidth, height: vizHeight, border: '1px solid green', margin: '0px' }}>
|
||||
// {JSON.stringify(scatterData, null, 2)}
|
||||
// </pre>
|
||||
<UPlotChart config={builder} data={facets!} width={vizWidth} height={vizHeight} timeRange={timeRange}>
|
||||
{/*children ? children(config, alignedFrame) : null*/}
|
||||
</UPlotChart>
|
||||
)}
|
||||
</VizLayout>
|
||||
<Portal>
|
||||
{hover && (
|
||||
<VizTooltipContainer position={{ x: hover.pageX, y: hover.pageY }} offset={{ x: 10, y: 10 }}>
|
||||
<TooltipView series={series[hover.scatterIndex]} rowIndex={hover.xIndex} data={data.series} />
|
||||
</VizTooltipContainer>
|
||||
)}
|
||||
</Portal>
|
||||
</>
|
||||
<div className="panel-empty">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<VizLayout width={props.width} height={props.height} legend={renderLegend()}>
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
<UPlotChart config={builder} data={facets} width={vizWidth} height={vizHeight} timeRange={props.timeRange} />
|
||||
)}
|
||||
</VizLayout>
|
||||
<Portal>
|
||||
{hover && props.options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||
<VizTooltipContainer
|
||||
position={{ x: hover.pageX, y: hover.pageY }}
|
||||
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
|
||||
allowPointerEvents={isToolTipOpen.current}
|
||||
>
|
||||
{shouldDisplayCloseButton && (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<CloseButton
|
||||
onClick={onCloseToolTip}
|
||||
style={{
|
||||
position: 'relative',
|
||||
top: 'auto',
|
||||
right: 'auto',
|
||||
marginRight: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TooltipView
|
||||
options={props.options.tooltip}
|
||||
allSeries={series}
|
||||
rowIndex={hover.xIndex}
|
||||
hoveredPointIndex={hover.scatterIndex}
|
||||
data={props.data.series}
|
||||
range={props.timeRange}
|
||||
/>
|
||||
</VizTooltipContainer>
|
||||
)}
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -12,8 +12,6 @@ import { LineStyleEditor } from '../timeseries/LineStyleEditor';
|
||||
|
||||
import { ScatterFieldConfig, ScatterLineMode } from './models.gen';
|
||||
|
||||
const categoryStyles = undefined; // ['Scatter styles'];
|
||||
|
||||
export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOptionsArgs<ScatterFieldConfig> {
|
||||
return {
|
||||
standardOptions: {
|
||||
@ -34,7 +32,6 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
||||
.addRadio({
|
||||
path: 'point',
|
||||
name: 'Points',
|
||||
category: categoryStyles,
|
||||
defaultValue: cfg.point,
|
||||
settings: {
|
||||
options: graphFieldOptions.showPoints,
|
||||
@ -42,8 +39,7 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'pointSize.fixed',
|
||||
name: 'Point size',
|
||||
category: categoryStyles,
|
||||
name: 'Size',
|
||||
defaultValue: cfg.pointSize?.fixed,
|
||||
settings: {
|
||||
min: 1,
|
||||
@ -55,7 +51,6 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
||||
.addRadio({
|
||||
path: 'line',
|
||||
name: 'Lines',
|
||||
category: categoryStyles,
|
||||
defaultValue: cfg.line,
|
||||
settings: {
|
||||
options: [
|
||||
@ -68,7 +63,6 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
||||
id: 'lineStyle',
|
||||
path: 'lineStyle',
|
||||
name: 'Line style',
|
||||
category: categoryStyles,
|
||||
showIf: (c) => c.line !== ScatterLineMode.None,
|
||||
editor: LineStyleEditor,
|
||||
override: LineStyleEditor,
|
||||
@ -78,7 +72,6 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
||||
.addSliderInput({
|
||||
path: 'lineWidth',
|
||||
name: 'Line width',
|
||||
category: categoryStyles,
|
||||
defaultValue: cfg.lineWidth,
|
||||
settings: {
|
||||
min: 0,
|
||||
|
@ -40,6 +40,7 @@ export interface ScatterFieldConfig extends HideableFieldConfig, AxisConfig {
|
||||
export interface ScatterSeriesConfig extends ScatterFieldConfig {
|
||||
x?: string;
|
||||
y?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const defaultScatterConfig: ScatterFieldConfig = {
|
||||
@ -65,7 +66,7 @@ export interface XYDimensionConfig {
|
||||
}
|
||||
|
||||
export interface XYChartOptions extends OptionsWithLegend, OptionsWithTooltip {
|
||||
mode?: 'xy' | 'explicit';
|
||||
mode?: 'auto' | 'manual';
|
||||
dims: XYDimensionConfig;
|
||||
|
||||
series?: ScatterSeriesConfig[];
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { commonOptionsBuilder } from '@grafana/ui';
|
||||
import { ColorDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors';
|
||||
|
||||
import { AutoEditor } from './AutoEditor';
|
||||
import { ManualEditor } from './ManualEditor';
|
||||
import { XYChartPanel2 } from './XYChartPanel2';
|
||||
import { XYDimsEditor } from './XYDimsEditor';
|
||||
import { getScatterFieldConfig } from './config';
|
||||
import { defaultScatterConfig, XYChartOptions, ScatterFieldConfig } from './models.gen';
|
||||
|
||||
@ -14,55 +14,28 @@ export const plugin = new PanelPlugin<XYChartOptions, ScatterFieldConfig>(XYChar
|
||||
.addRadio({
|
||||
path: 'mode',
|
||||
name: 'Mode',
|
||||
defaultValue: 'single',
|
||||
defaultValue: 'auto',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: 'xy', label: 'XY', description: 'No changes to saved model since 8.0' },
|
||||
{ value: 'explicit', label: 'Explicit' },
|
||||
{ value: 'auto', label: 'Auto', description: 'No changes to saved model since 8.0' },
|
||||
{ value: 'manual', label: 'Manual' },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'xyPlotConfig',
|
||||
path: 'dims',
|
||||
name: 'Data',
|
||||
editor: XYDimsEditor,
|
||||
showIf: (cfg) => cfg.mode === 'xy',
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'series[0].x',
|
||||
name: 'X Field',
|
||||
showIf: (cfg) => cfg.mode === 'explicit',
|
||||
})
|
||||
.addFieldNamePicker({
|
||||
path: 'series[0].y',
|
||||
name: 'Y Field',
|
||||
showIf: (cfg) => cfg.mode === 'explicit',
|
||||
name: '',
|
||||
editor: AutoEditor,
|
||||
showIf: (cfg) => cfg.mode === 'auto',
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'seriesZerox.pointColor',
|
||||
path: 'series[0].pointColor',
|
||||
name: 'Point color',
|
||||
editor: ColorDimensionEditor,
|
||||
settings: {},
|
||||
defaultValue: {},
|
||||
showIf: (cfg) => cfg.mode === 'explicit',
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'seriesZerox.pointSize',
|
||||
path: 'series[0].pointSize',
|
||||
name: 'Point size',
|
||||
editor: ScaleDimensionEditor,
|
||||
settings: {
|
||||
min: 1,
|
||||
max: 50,
|
||||
},
|
||||
defaultValue: {
|
||||
fixed: 5,
|
||||
min: 1,
|
||||
max: 50,
|
||||
},
|
||||
showIf: (cfg) => cfg.mode === 'explicit',
|
||||
id: 'series',
|
||||
path: 'series',
|
||||
name: '',
|
||||
defaultValue: [],
|
||||
editor: ManualEditor,
|
||||
showIf: (cfg) => cfg.mode === 'manual',
|
||||
});
|
||||
|
||||
commonOptionsBuilder.addTooltipOptions(builder);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { MutableRefObject } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
@ -41,19 +42,26 @@ export function prepScatter(
|
||||
options: XYChartOptions,
|
||||
getData: () => DataFrame[],
|
||||
theme: GrafanaTheme2,
|
||||
ttip: ScatterHoverCallback
|
||||
ttip: ScatterHoverCallback,
|
||||
onUPlotClick: null | ((evt?: Object) => void),
|
||||
isToolTipOpen: MutableRefObject<boolean>
|
||||
): ScatterPanelInfo {
|
||||
let series: ScatterSeries[];
|
||||
let builder: UPlotConfigBuilder;
|
||||
|
||||
try {
|
||||
series = prepSeries(options, getData());
|
||||
builder = prepConfig(getData, series, theme, ttip);
|
||||
builder = prepConfig(getData, series, theme, ttip, onUPlotClick, isToolTipOpen);
|
||||
} catch (e) {
|
||||
console.log('prepScatter ERROR', e);
|
||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error in prepScatter';
|
||||
let errorMsg = 'Unknown error in prepScatter';
|
||||
if (typeof e === 'string') {
|
||||
errorMsg = e;
|
||||
} else if (e instanceof Error) {
|
||||
errorMsg = e.message;
|
||||
}
|
||||
|
||||
return {
|
||||
error: errorMessage,
|
||||
error: errorMsg,
|
||||
series: [],
|
||||
};
|
||||
}
|
||||
@ -120,7 +128,7 @@ function getScatterSeries(
|
||||
// Size configs
|
||||
//----------------
|
||||
let pointSizeHints = dims.pointSizeConfig;
|
||||
let pointSizeFixed = dims.pointSizeConfig?.fixed ?? y.config.custom?.pointSizeConfig?.fixed ?? 5;
|
||||
let pointSizeFixed = dims.pointSizeConfig?.fixed ?? y.config.custom?.pointSize?.fixed ?? 5;
|
||||
let pointSize: DimensionValues<number> = () => pointSizeFixed;
|
||||
if (dims.pointSizeIndex) {
|
||||
pointSize = (frame) => {
|
||||
@ -176,6 +184,7 @@ function getScatterSeries(
|
||||
|
||||
label: VisibilityMode.Never,
|
||||
labelValue: () => '',
|
||||
show: !frame.fields[yIndex].config.custom.hideFrom?.viz,
|
||||
|
||||
hints: {
|
||||
pointSize: pointSizeHints!,
|
||||
@ -189,11 +198,13 @@ function getScatterSeries(
|
||||
function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries[] {
|
||||
let seriesIndex = 0;
|
||||
if (!frames.length) {
|
||||
throw 'missing data';
|
||||
throw 'Missing data';
|
||||
}
|
||||
|
||||
if (options.mode === 'explicit') {
|
||||
if (options.mode === 'manual') {
|
||||
if (options.series?.length) {
|
||||
const scatterSeries: ScatterSeries[] = [];
|
||||
|
||||
for (const series of options.series) {
|
||||
if (!series?.x) {
|
||||
throw 'Select X dimension';
|
||||
@ -221,10 +232,12 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
||||
pointSizeConfig: series.pointSize,
|
||||
pointSizeIndex: findFieldIndex(frame, series.pointSize?.field),
|
||||
};
|
||||
return [getScatterSeries(seriesIndex++, frames, frameIndex, xIndex, yIndex, dims)];
|
||||
scatterSeries.push(getScatterSeries(seriesIndex++, frames, frameIndex, xIndex, yIndex, dims));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scatterSeries;
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,7 +245,7 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
||||
const dims = options.dims ?? {};
|
||||
const frameIndex = dims.frame ?? 0;
|
||||
const frame = frames[frameIndex];
|
||||
const numericIndicies: number[] = [];
|
||||
const numericIndices: number[] = [];
|
||||
|
||||
let xIndex = findFieldIndex(frame, dims.x);
|
||||
for (let i = 0; i < frame.fields.length; i++) {
|
||||
@ -245,7 +258,7 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
||||
continue; // skip
|
||||
}
|
||||
|
||||
numericIndicies.push(i);
|
||||
numericIndices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,10 +266,10 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
||||
throw 'Missing X dimension';
|
||||
}
|
||||
|
||||
if (!numericIndicies.length) {
|
||||
if (!numericIndices.length) {
|
||||
throw 'No Y values';
|
||||
}
|
||||
return numericIndicies.map((yIndex) => getScatterSeries(seriesIndex++, frames, frameIndex, xIndex!, yIndex, {}));
|
||||
return numericIndices.map((yIndex) => getScatterSeries(seriesIndex++, frames, frameIndex, xIndex!, yIndex, {}));
|
||||
}
|
||||
|
||||
interface DrawBubblesOpts {
|
||||
@ -278,7 +291,9 @@ const prepConfig = (
|
||||
getData: () => DataFrame[],
|
||||
scatterSeries: ScatterSeries[],
|
||||
theme: GrafanaTheme2,
|
||||
ttip: ScatterHoverCallback
|
||||
ttip: ScatterHoverCallback,
|
||||
onUPlotClick: null | ((evt?: Object) => void),
|
||||
isToolTipOpen: MutableRefObject<boolean>
|
||||
) => {
|
||||
let qt: Quadtree;
|
||||
let hRect: Rect | null;
|
||||
@ -489,9 +504,32 @@ const prepConfig = (
|
||||
},
|
||||
});
|
||||
|
||||
const clearPopupIfOpened = () => {
|
||||
if (isToolTipOpen.current) {
|
||||
ttip(undefined);
|
||||
if (onUPlotClick) {
|
||||
onUPlotClick();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ref_parent: HTMLElement | null = null;
|
||||
|
||||
// clip hover points/bubbles to plotting area
|
||||
builder.addHook('init', (u, r) => {
|
||||
u.over.style.overflow = 'hidden';
|
||||
ref_parent = u.root.parentElement;
|
||||
|
||||
if (onUPlotClick) {
|
||||
ref_parent?.addEventListener('click', onUPlotClick);
|
||||
}
|
||||
});
|
||||
|
||||
builder.addHook('destroy', (u) => {
|
||||
if (onUPlotClick) {
|
||||
ref_parent?.removeEventListener('click', onUPlotClick);
|
||||
clearPopupIfOpened();
|
||||
}
|
||||
});
|
||||
|
||||
let rect: DOMRect;
|
||||
@ -502,11 +540,10 @@ const prepConfig = (
|
||||
});
|
||||
|
||||
builder.addHook('setLegend', (u) => {
|
||||
// console.log('TTIP???', u.cursor.idxs);
|
||||
if (u.cursor.idxs != null) {
|
||||
for (let i = 0; i < u.cursor.idxs.length; i++) {
|
||||
const sel = u.cursor.idxs[i];
|
||||
if (sel != null) {
|
||||
if (sel != null && !isToolTipOpen.current) {
|
||||
ttip({
|
||||
scatterIndex: i - 1,
|
||||
xIndex: sel,
|
||||
@ -517,10 +554,15 @@ const prepConfig = (
|
||||
}
|
||||
}
|
||||
}
|
||||
ttip(undefined);
|
||||
|
||||
if (!isToolTipOpen.current) {
|
||||
ttip(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
builder.addHook('drawClear', (u) => {
|
||||
clearPopupIfOpened();
|
||||
|
||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||
|
||||
qt.clear();
|
||||
@ -600,6 +642,7 @@ const prepConfig = (
|
||||
scaleKey: '', // facets' scales used (above)
|
||||
lineColor: lineColor as string,
|
||||
fillColor: alpha(pointColor, 0.5),
|
||||
show: !field.config.custom.hideFrom?.viz,
|
||||
});
|
||||
});
|
||||
|
||||
@ -634,7 +677,7 @@ const prepConfig = (
|
||||
* from? is this where we would support that? -- need the previous values
|
||||
*/
|
||||
export function prepData(info: ScatterPanelInfo, data: DataFrame[], from?: number): FacetedData {
|
||||
if (info.error) {
|
||||
if (info.error || !data.length) {
|
||||
return [null];
|
||||
}
|
||||
return [
|
||||
|
@ -51,6 +51,7 @@ export interface ScatterSeries {
|
||||
|
||||
label: VisibilityMode;
|
||||
labelValue: DimensionValues<string>;
|
||||
show: boolean;
|
||||
|
||||
hints: {
|
||||
pointSize: ScaleDimensionConfig;
|
||||
|
Loading…
Reference in New Issue
Block a user