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, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
[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"]
|
[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"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/panel/xychart/dims.ts:5381": [
|
"public/app/plugins/panel/xychart/dims.ts:5381": [
|
||||||
|
@ -234,7 +234,6 @@ export class TestDataDataSource extends DataSourceWithBackend<TestDataQuery> {
|
|||||||
options: DataQueryRequest<TestDataQuery>
|
options: DataQueryRequest<TestDataQuery>
|
||||||
): Observable<DataQueryResponse> | null {
|
): Observable<DataQueryResponse> | null {
|
||||||
const { errorType } = target;
|
const { errorType } = target;
|
||||||
console.log("we're here!", target);
|
|
||||||
|
|
||||||
if (errorType === 'server_panic') {
|
if (errorType === 'server_panic') {
|
||||||
return null;
|
return null;
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
} from '@grafana/data';
|
} 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 { getXYDimensions, isGraphable } from './dims';
|
||||||
import { XYDimensionConfig, XYChartOptions } from './models.gen';
|
import { XYDimensionConfig, XYChartOptions } from './models.gen';
|
||||||
@ -19,7 +19,7 @@ interface XYInfo {
|
|||||||
yFields: Array<SelectableValue<boolean>>;
|
yFields: Array<SelectableValue<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const XYDimsEditor: FC<StandardEditorProps<XYDimensionConfig, any, XYChartOptions>> = ({
|
export const AutoEditor: FC<StandardEditorProps<XYDimensionConfig, any, XYChartOptions>> = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
context,
|
context,
|
||||||
@ -89,54 +89,55 @@ export const XYDimsEditor: FC<StandardEditorProps<XYDimensionConfig, any, XYChar
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Field label={'Data'}>
|
||||||
options={frameNames}
|
<Select
|
||||||
value={frameNames.find((v) => v.value === value?.frame) ?? frameNames[0]}
|
options={frameNames}
|
||||||
onChange={(v) => {
|
value={frameNames.find((v) => v.value === value?.frame) ?? frameNames[0]}
|
||||||
onChange({
|
onChange={(v) => {
|
||||||
...value,
|
onChange({
|
||||||
frame: v.value!,
|
...value,
|
||||||
});
|
frame: v.value!,
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
<br />
|
/>
|
||||||
<Label>X Field</Label>
|
</Field>
|
||||||
<Select
|
<Field label={'X Field'}>
|
||||||
options={info.numberFields}
|
<Select
|
||||||
value={info.xAxis}
|
options={info.numberFields}
|
||||||
onChange={(v) => {
|
value={info.xAxis}
|
||||||
onChange({
|
onChange={(v) => {
|
||||||
...value,
|
onChange({
|
||||||
x: v.value,
|
...value,
|
||||||
});
|
x: v.value,
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
<br />
|
/>
|
||||||
<Label>Y Fields</Label>
|
</Field>
|
||||||
<div>
|
<Field label={'Y Fields'}>
|
||||||
{info.yFields.map((v) => (
|
<div>
|
||||||
<div key={v.label} className={styles.row}>
|
{info.yFields.map((v) => (
|
||||||
<IconButton
|
<div key={v.label} className={styles.row}>
|
||||||
name={v.value ? 'eye-slash' : 'eye'}
|
<IconButton
|
||||||
onClick={() => {
|
name={v.value ? 'eye-slash' : 'eye'}
|
||||||
const exclude: string[] = value?.exclude ? [...value.exclude] : [];
|
onClick={() => {
|
||||||
let idx = exclude.indexOf(v.label!);
|
const exclude: string[] = value?.exclude ? [...value.exclude] : [];
|
||||||
if (idx < 0) {
|
let idx = exclude.indexOf(v.label!);
|
||||||
exclude.push(v.label!);
|
if (idx < 0) {
|
||||||
} else {
|
exclude.push(v.label!);
|
||||||
exclude.splice(idx, 1);
|
} else {
|
||||||
}
|
exclude.splice(idx, 1);
|
||||||
onChange({
|
}
|
||||||
...value,
|
onChange({
|
||||||
exclude,
|
...value,
|
||||||
});
|
exclude,
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
{v.label}
|
/>
|
||||||
</div>
|
{v.label}
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
<br /> <br />
|
</div>
|
||||||
|
</Field>
|
||||||
</div>
|
</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 { css } from '@emotion/css';
|
||||||
import React, { PureComponent } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { DataFrame, Field, formattedValueToString, getFieldDisplayName, GrafanaTheme2 } from '@grafana/data';
|
import {
|
||||||
import { stylesFactory } from '@grafana/ui';
|
arrayUtils,
|
||||||
import { config } from 'app/core/config';
|
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';
|
import { ScatterSeries } from './types';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
series: ScatterSeries;
|
allSeries: ScatterSeries[];
|
||||||
data: DataFrame[]; // source data
|
data: DataFrame[]; // source data
|
||||||
rowIndex?: number; // the hover row
|
rowIndex?: number; // the hover row
|
||||||
|
hoveredPointIndex: number; // the hovered point
|
||||||
|
options: VizTooltipOptions;
|
||||||
|
range: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TooltipView extends PureComponent<Props> {
|
export const TooltipView = ({ allSeries, data, rowIndex, hoveredPointIndex, options, range }: Props) => {
|
||||||
style = getStyles(config.theme2);
|
const style = useStyles2(getStyles);
|
||||||
|
const { onSplitOpen } = usePanelContext();
|
||||||
|
|
||||||
render() {
|
if (!allSeries || rowIndex == null) {
|
||||||
const { series, data, rowIndex } = this.props;
|
return null;
|
||||||
if (!series || rowIndex == null) {
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const frame = series.frame(data);
|
|
||||||
const y = undefined; // series.y(frame);
|
|
||||||
|
|
||||||
return (
|
const series = allSeries[hoveredPointIndex];
|
||||||
<table className={this.style.infoWrap}>
|
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>
|
<tbody>
|
||||||
{frame.fields.map((f, i) => (
|
{yValues.map((el, index) => {
|
||||||
<tr key={`${i}/${rowIndex}`} className={f === y ? this.style.highlight : ''}>
|
let color = null;
|
||||||
<th>{getFieldDisplayName(f, frame)}:</th>
|
if (typeof el!.color === 'string') {
|
||||||
<td>{fmt(f, rowIndex)}</td>
|
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>
|
</tr>
|
||||||
))}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
);
|
</>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function fmt(field: Field, row: number): string {
|
function fmt(field: Field, val: number): string {
|
||||||
const v = field.values.get(row);
|
|
||||||
if (field.display) {
|
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`
|
infoWrap: css`
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
th {
|
th {
|
||||||
font-weight: ${theme.typography.fontWeightMedium};
|
font-weight: ${theme.typography.fontWeightMedium};
|
||||||
padding: ${theme.spacing(0.25, 2)};
|
padding: ${theme.spacing(0.25, 2)};
|
||||||
@ -58,4 +165,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
|||||||
highlight: css`
|
highlight: css`
|
||||||
background: ${theme.colors.action.hover};
|
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 { config } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
LegendDisplayMode,
|
|
||||||
Portal,
|
Portal,
|
||||||
|
TooltipDisplayMode,
|
||||||
UPlotChart,
|
UPlotChart,
|
||||||
UPlotConfigBuilder,
|
UPlotConfigBuilder,
|
||||||
VizLayout,
|
VizLayout,
|
||||||
@ -13,116 +23,231 @@ import {
|
|||||||
VizTooltipContainer,
|
VizTooltipContainer,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { FacetedData } from '@grafana/ui/src/components/uPlot/types';
|
import { FacetedData } from '@grafana/ui/src/components/uPlot/types';
|
||||||
|
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||||
|
|
||||||
import { TooltipView } from './TooltipView';
|
import { TooltipView } from './TooltipView';
|
||||||
import { XYChartOptions } from './models.gen';
|
import { XYChartOptions } from './models.gen';
|
||||||
import { prepData, prepScatter } from './scatter';
|
import { prepData, prepScatter, ScatterPanelInfo } from './scatter';
|
||||||
import { ScatterHoverEvent, ScatterSeries } from './types';
|
import { ScatterHoverEvent, ScatterSeries } from './types';
|
||||||
|
|
||||||
type Props = PanelProps<XYChartOptions>;
|
type Props = PanelProps<XYChartOptions>;
|
||||||
type State = {
|
const TOOLTIP_OFFSET = 10;
|
||||||
error?: string;
|
|
||||||
series: ScatterSeries[];
|
|
||||||
builder?: UPlotConfigBuilder;
|
|
||||||
facets?: FacetedData;
|
|
||||||
hover?: ScatterHoverEvent;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class XYChartPanel2 extends PureComponent<Props, State> {
|
export const XYChartPanel2: React.FC<Props> = (props: Props) => {
|
||||||
state: State = {
|
const [error, setError] = useState<string | undefined>();
|
||||||
series: [],
|
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() {
|
const onUPlotClick = () => {
|
||||||
this.initSeries(); // also data
|
isToolTipOpen.current = !isToolTipOpen.current;
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(oldProps: Props) {
|
// Linking into useState required to re-render tooltip
|
||||||
const { options, data } = this.props;
|
setShouldDisplayCloseButton(isToolTipOpen.current);
|
||||||
const configsChanged = options !== oldProps.options || data.structureRev !== oldProps.data.structureRev;
|
};
|
||||||
|
|
||||||
if (configsChanged) {
|
const scatterHoverCallback = (hover?: ScatterHoverEvent) => {
|
||||||
this.initSeries();
|
setHover(hover);
|
||||||
} else if (data !== oldProps.data) {
|
};
|
||||||
this.initFacets();
|
|
||||||
|
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) => {
|
const initFacets = useCallback(() => {
|
||||||
this.setState({ hover });
|
setFacets(() => prepData({ error, series }, props.data.series));
|
||||||
};
|
}, [props.data.series, error, series]);
|
||||||
|
|
||||||
getData = () => {
|
useEffect(() => {
|
||||||
return this.props.data.series;
|
if (oldOptions !== props.options || oldData?.structureRev !== props.data.structureRev) {
|
||||||
};
|
initSeries();
|
||||||
|
} else if (oldData !== props.data) {
|
||||||
initSeries = () => {
|
initFacets();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
this.setState(info);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
};
|
}, [props]);
|
||||||
|
|
||||||
initFacets = () => {
|
const renderLegend = () => {
|
||||||
this.setState({
|
|
||||||
facets: prepData(this.state, this.props.data.series),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderLegend = () => {
|
|
||||||
const { data } = this.props;
|
|
||||||
const { series } = this.state;
|
|
||||||
const items: VizLegendItem[] = [];
|
const items: VizLegendItem[] = [];
|
||||||
|
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||||
|
const theme = config.theme2;
|
||||||
|
|
||||||
for (const s of series) {
|
for (const s of series) {
|
||||||
const frame = s.frame(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(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);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!props.options.legend.showLegend) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legendStyle = {
|
||||||
|
flexStart: css`
|
||||||
|
div {
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VizLayout.Legend placement="bottom">
|
<VizLayout.Legend placement={props.options.legend.placement} width={props.options.legend.width}>
|
||||||
<VizLegend placement="bottom" items={items} displayMode={LegendDisplayMode.List} />
|
<VizLegend
|
||||||
|
className={legendStyle.flexStart}
|
||||||
|
placement={props.options.legend.placement}
|
||||||
|
items={items}
|
||||||
|
displayMode={props.options.legend.displayMode}
|
||||||
|
/>
|
||||||
</VizLayout.Legend>
|
</VizLayout.Legend>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
if (error || !builder || !facets) {
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="panel-empty">
|
||||||
<VizLayout width={width} height={height} legend={this.renderLegend()}>
|
<p>{error}</p>
|
||||||
{(vizWidth: number, vizHeight: number) => (
|
</div>
|
||||||
// <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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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';
|
import { ScatterFieldConfig, ScatterLineMode } from './models.gen';
|
||||||
|
|
||||||
const categoryStyles = undefined; // ['Scatter styles'];
|
|
||||||
|
|
||||||
export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOptionsArgs<ScatterFieldConfig> {
|
export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOptionsArgs<ScatterFieldConfig> {
|
||||||
return {
|
return {
|
||||||
standardOptions: {
|
standardOptions: {
|
||||||
@ -34,7 +32,6 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
|||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'point',
|
path: 'point',
|
||||||
name: 'Points',
|
name: 'Points',
|
||||||
category: categoryStyles,
|
|
||||||
defaultValue: cfg.point,
|
defaultValue: cfg.point,
|
||||||
settings: {
|
settings: {
|
||||||
options: graphFieldOptions.showPoints,
|
options: graphFieldOptions.showPoints,
|
||||||
@ -42,8 +39,7 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
|||||||
})
|
})
|
||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
path: 'pointSize.fixed',
|
path: 'pointSize.fixed',
|
||||||
name: 'Point size',
|
name: 'Size',
|
||||||
category: categoryStyles,
|
|
||||||
defaultValue: cfg.pointSize?.fixed,
|
defaultValue: cfg.pointSize?.fixed,
|
||||||
settings: {
|
settings: {
|
||||||
min: 1,
|
min: 1,
|
||||||
@ -55,7 +51,6 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
|||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'line',
|
path: 'line',
|
||||||
name: 'Lines',
|
name: 'Lines',
|
||||||
category: categoryStyles,
|
|
||||||
defaultValue: cfg.line,
|
defaultValue: cfg.line,
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
@ -68,7 +63,6 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
|||||||
id: 'lineStyle',
|
id: 'lineStyle',
|
||||||
path: 'lineStyle',
|
path: 'lineStyle',
|
||||||
name: 'Line style',
|
name: 'Line style',
|
||||||
category: categoryStyles,
|
|
||||||
showIf: (c) => c.line !== ScatterLineMode.None,
|
showIf: (c) => c.line !== ScatterLineMode.None,
|
||||||
editor: LineStyleEditor,
|
editor: LineStyleEditor,
|
||||||
override: LineStyleEditor,
|
override: LineStyleEditor,
|
||||||
@ -78,7 +72,6 @@ export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOp
|
|||||||
.addSliderInput({
|
.addSliderInput({
|
||||||
path: 'lineWidth',
|
path: 'lineWidth',
|
||||||
name: 'Line width',
|
name: 'Line width',
|
||||||
category: categoryStyles,
|
|
||||||
defaultValue: cfg.lineWidth,
|
defaultValue: cfg.lineWidth,
|
||||||
settings: {
|
settings: {
|
||||||
min: 0,
|
min: 0,
|
||||||
|
@ -40,6 +40,7 @@ export interface ScatterFieldConfig extends HideableFieldConfig, AxisConfig {
|
|||||||
export interface ScatterSeriesConfig extends ScatterFieldConfig {
|
export interface ScatterSeriesConfig extends ScatterFieldConfig {
|
||||||
x?: string;
|
x?: string;
|
||||||
y?: string;
|
y?: string;
|
||||||
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultScatterConfig: ScatterFieldConfig = {
|
export const defaultScatterConfig: ScatterFieldConfig = {
|
||||||
@ -65,7 +66,7 @@ export interface XYDimensionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface XYChartOptions extends OptionsWithLegend, OptionsWithTooltip {
|
export interface XYChartOptions extends OptionsWithLegend, OptionsWithTooltip {
|
||||||
mode?: 'xy' | 'explicit';
|
mode?: 'auto' | 'manual';
|
||||||
dims: XYDimensionConfig;
|
dims: XYDimensionConfig;
|
||||||
|
|
||||||
series?: ScatterSeriesConfig[];
|
series?: ScatterSeriesConfig[];
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { PanelPlugin } from '@grafana/data';
|
import { PanelPlugin } from '@grafana/data';
|
||||||
import { commonOptionsBuilder } from '@grafana/ui';
|
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 { XYChartPanel2 } from './XYChartPanel2';
|
||||||
import { XYDimsEditor } from './XYDimsEditor';
|
|
||||||
import { getScatterFieldConfig } from './config';
|
import { getScatterFieldConfig } from './config';
|
||||||
import { defaultScatterConfig, XYChartOptions, ScatterFieldConfig } from './models.gen';
|
import { defaultScatterConfig, XYChartOptions, ScatterFieldConfig } from './models.gen';
|
||||||
|
|
||||||
@ -14,55 +14,28 @@ export const plugin = new PanelPlugin<XYChartOptions, ScatterFieldConfig>(XYChar
|
|||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'mode',
|
path: 'mode',
|
||||||
name: 'Mode',
|
name: 'Mode',
|
||||||
defaultValue: 'single',
|
defaultValue: 'auto',
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
{ value: 'xy', label: 'XY', description: 'No changes to saved model since 8.0' },
|
{ value: 'auto', label: 'Auto', description: 'No changes to saved model since 8.0' },
|
||||||
{ value: 'explicit', label: 'Explicit' },
|
{ value: 'manual', label: 'Manual' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
id: 'xyPlotConfig',
|
id: 'xyPlotConfig',
|
||||||
path: 'dims',
|
path: 'dims',
|
||||||
name: 'Data',
|
name: '',
|
||||||
editor: XYDimsEditor,
|
editor: AutoEditor,
|
||||||
showIf: (cfg) => cfg.mode === 'xy',
|
showIf: (cfg) => cfg.mode === 'auto',
|
||||||
})
|
|
||||||
.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',
|
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
id: 'seriesZerox.pointColor',
|
id: 'series',
|
||||||
path: 'series[0].pointColor',
|
path: 'series',
|
||||||
name: 'Point color',
|
name: '',
|
||||||
editor: ColorDimensionEditor,
|
defaultValue: [],
|
||||||
settings: {},
|
editor: ManualEditor,
|
||||||
defaultValue: {},
|
showIf: (cfg) => cfg.mode === 'manual',
|
||||||
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',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
commonOptionsBuilder.addTooltipOptions(builder);
|
commonOptionsBuilder.addTooltipOptions(builder);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MutableRefObject } from 'react';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -41,19 +42,26 @@ export function prepScatter(
|
|||||||
options: XYChartOptions,
|
options: XYChartOptions,
|
||||||
getData: () => DataFrame[],
|
getData: () => DataFrame[],
|
||||||
theme: GrafanaTheme2,
|
theme: GrafanaTheme2,
|
||||||
ttip: ScatterHoverCallback
|
ttip: ScatterHoverCallback,
|
||||||
|
onUPlotClick: null | ((evt?: Object) => void),
|
||||||
|
isToolTipOpen: MutableRefObject<boolean>
|
||||||
): ScatterPanelInfo {
|
): ScatterPanelInfo {
|
||||||
let series: ScatterSeries[];
|
let series: ScatterSeries[];
|
||||||
let builder: UPlotConfigBuilder;
|
let builder: UPlotConfigBuilder;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
series = prepSeries(options, getData());
|
series = prepSeries(options, getData());
|
||||||
builder = prepConfig(getData, series, theme, ttip);
|
builder = prepConfig(getData, series, theme, ttip, onUPlotClick, isToolTipOpen);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('prepScatter ERROR', e);
|
let errorMsg = 'Unknown error in prepScatter';
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Unknown error in prepScatter';
|
if (typeof e === 'string') {
|
||||||
|
errorMsg = e;
|
||||||
|
} else if (e instanceof Error) {
|
||||||
|
errorMsg = e.message;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: errorMessage,
|
error: errorMsg,
|
||||||
series: [],
|
series: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -120,7 +128,7 @@ function getScatterSeries(
|
|||||||
// Size configs
|
// Size configs
|
||||||
//----------------
|
//----------------
|
||||||
let pointSizeHints = dims.pointSizeConfig;
|
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;
|
let pointSize: DimensionValues<number> = () => pointSizeFixed;
|
||||||
if (dims.pointSizeIndex) {
|
if (dims.pointSizeIndex) {
|
||||||
pointSize = (frame) => {
|
pointSize = (frame) => {
|
||||||
@ -176,6 +184,7 @@ function getScatterSeries(
|
|||||||
|
|
||||||
label: VisibilityMode.Never,
|
label: VisibilityMode.Never,
|
||||||
labelValue: () => '',
|
labelValue: () => '',
|
||||||
|
show: !frame.fields[yIndex].config.custom.hideFrom?.viz,
|
||||||
|
|
||||||
hints: {
|
hints: {
|
||||||
pointSize: pointSizeHints!,
|
pointSize: pointSizeHints!,
|
||||||
@ -189,11 +198,13 @@ function getScatterSeries(
|
|||||||
function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries[] {
|
function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries[] {
|
||||||
let seriesIndex = 0;
|
let seriesIndex = 0;
|
||||||
if (!frames.length) {
|
if (!frames.length) {
|
||||||
throw 'missing data';
|
throw 'Missing data';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.mode === 'explicit') {
|
if (options.mode === 'manual') {
|
||||||
if (options.series?.length) {
|
if (options.series?.length) {
|
||||||
|
const scatterSeries: ScatterSeries[] = [];
|
||||||
|
|
||||||
for (const series of options.series) {
|
for (const series of options.series) {
|
||||||
if (!series?.x) {
|
if (!series?.x) {
|
||||||
throw 'Select X dimension';
|
throw 'Select X dimension';
|
||||||
@ -221,10 +232,12 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
|||||||
pointSizeConfig: series.pointSize,
|
pointSizeConfig: series.pointSize,
|
||||||
pointSizeIndex: findFieldIndex(frame, series.pointSize?.field),
|
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 dims = options.dims ?? {};
|
||||||
const frameIndex = dims.frame ?? 0;
|
const frameIndex = dims.frame ?? 0;
|
||||||
const frame = frames[frameIndex];
|
const frame = frames[frameIndex];
|
||||||
const numericIndicies: number[] = [];
|
const numericIndices: number[] = [];
|
||||||
|
|
||||||
let xIndex = findFieldIndex(frame, dims.x);
|
let xIndex = findFieldIndex(frame, dims.x);
|
||||||
for (let i = 0; i < frame.fields.length; i++) {
|
for (let i = 0; i < frame.fields.length; i++) {
|
||||||
@ -245,7 +258,7 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
|||||||
continue; // skip
|
continue; // skip
|
||||||
}
|
}
|
||||||
|
|
||||||
numericIndicies.push(i);
|
numericIndices.push(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,10 +266,10 @@ function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries
|
|||||||
throw 'Missing X dimension';
|
throw 'Missing X dimension';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!numericIndicies.length) {
|
if (!numericIndices.length) {
|
||||||
throw 'No Y values';
|
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 {
|
interface DrawBubblesOpts {
|
||||||
@ -278,7 +291,9 @@ const prepConfig = (
|
|||||||
getData: () => DataFrame[],
|
getData: () => DataFrame[],
|
||||||
scatterSeries: ScatterSeries[],
|
scatterSeries: ScatterSeries[],
|
||||||
theme: GrafanaTheme2,
|
theme: GrafanaTheme2,
|
||||||
ttip: ScatterHoverCallback
|
ttip: ScatterHoverCallback,
|
||||||
|
onUPlotClick: null | ((evt?: Object) => void),
|
||||||
|
isToolTipOpen: MutableRefObject<boolean>
|
||||||
) => {
|
) => {
|
||||||
let qt: Quadtree;
|
let qt: Quadtree;
|
||||||
let hRect: Rect | null;
|
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
|
// clip hover points/bubbles to plotting area
|
||||||
builder.addHook('init', (u, r) => {
|
builder.addHook('init', (u, r) => {
|
||||||
u.over.style.overflow = 'hidden';
|
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;
|
let rect: DOMRect;
|
||||||
@ -502,11 +540,10 @@ const prepConfig = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.addHook('setLegend', (u) => {
|
builder.addHook('setLegend', (u) => {
|
||||||
// console.log('TTIP???', u.cursor.idxs);
|
|
||||||
if (u.cursor.idxs != null) {
|
if (u.cursor.idxs != null) {
|
||||||
for (let i = 0; i < u.cursor.idxs.length; i++) {
|
for (let i = 0; i < u.cursor.idxs.length; i++) {
|
||||||
const sel = u.cursor.idxs[i];
|
const sel = u.cursor.idxs[i];
|
||||||
if (sel != null) {
|
if (sel != null && !isToolTipOpen.current) {
|
||||||
ttip({
|
ttip({
|
||||||
scatterIndex: i - 1,
|
scatterIndex: i - 1,
|
||||||
xIndex: sel,
|
xIndex: sel,
|
||||||
@ -517,10 +554,15 @@ const prepConfig = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ttip(undefined);
|
|
||||||
|
if (!isToolTipOpen.current) {
|
||||||
|
ttip(undefined);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addHook('drawClear', (u) => {
|
builder.addHook('drawClear', (u) => {
|
||||||
|
clearPopupIfOpened();
|
||||||
|
|
||||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||||
|
|
||||||
qt.clear();
|
qt.clear();
|
||||||
@ -600,6 +642,7 @@ const prepConfig = (
|
|||||||
scaleKey: '', // facets' scales used (above)
|
scaleKey: '', // facets' scales used (above)
|
||||||
lineColor: lineColor as string,
|
lineColor: lineColor as string,
|
||||||
fillColor: alpha(pointColor, 0.5),
|
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
|
* from? is this where we would support that? -- need the previous values
|
||||||
*/
|
*/
|
||||||
export function prepData(info: ScatterPanelInfo, data: DataFrame[], from?: number): FacetedData {
|
export function prepData(info: ScatterPanelInfo, data: DataFrame[], from?: number): FacetedData {
|
||||||
if (info.error) {
|
if (info.error || !data.length) {
|
||||||
return [null];
|
return [null];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
@ -51,6 +51,7 @@ export interface ScatterSeries {
|
|||||||
|
|
||||||
label: VisibilityMode;
|
label: VisibilityMode;
|
||||||
labelValue: DimensionValues<string>;
|
labelValue: DimensionValues<string>;
|
||||||
|
show: boolean;
|
||||||
|
|
||||||
hints: {
|
hints: {
|
||||||
pointSize: ScaleDimensionConfig;
|
pointSize: ScaleDimensionConfig;
|
||||||
|
Loading…
Reference in New Issue
Block a user