mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
XYChart: Refactor to new model (behind feature toggle) (#82499)
Co-authored-by: drew08t <drew08@gmail.com> Co-authored-by: Ihor Yeromin <yeryomin.igor@gmail.com>
This commit is contained in:
@@ -129,6 +129,9 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||||
],
|
],
|
||||||
|
"packages/grafana-data/src/field/fieldState.ts:5381": [
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
|
],
|
||||||
"packages/grafana-data/src/field/overrides/processors.ts:5381": [
|
"packages/grafana-data/src/field/overrides/processors.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
@@ -5943,6 +5946,32 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "10"]
|
[0, 0, 0, "Do not use any type assertions.", "10"]
|
||||||
],
|
],
|
||||||
|
"public/app/plugins/panel/xychart/v2/SeriesEditor.tsx:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "4"]
|
||||||
|
],
|
||||||
|
"public/app/plugins/panel/xychart/v2/migrations.ts:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
|
],
|
||||||
|
"public/app/plugins/panel/xychart/v2/scatter.ts:5381": [
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
|
[0, 0, 0, "Do not use any type assertions.", "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"]
|
||||||
|
],
|
||||||
|
"public/app/plugins/panel/xychart/v2/utils.ts:5381": [
|
||||||
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
|
],
|
||||||
"public/app/store/configureStore.ts:5381": [
|
"public/app/store/configureStore.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -206,7 +206,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Male",
|
"name": "Male",
|
||||||
"pointColor": {
|
"pointColor": {
|
||||||
"fixed": "#5795f200"
|
"fixed": "#5795f2"
|
||||||
},
|
},
|
||||||
"pointSize": {
|
"pointSize": {
|
||||||
"fixed": 5,
|
"fixed": 5,
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Female",
|
"name": "Female",
|
||||||
"pointColor": {
|
"pointColor": {
|
||||||
"fixed": "#ff983000"
|
"fixed": "#ff9830"
|
||||||
},
|
},
|
||||||
"pointSize": {
|
"pointSize": {
|
||||||
"fixed": 5,
|
"fixed": 5,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ Some features are enabled by default. You can disable these feature by setting t
|
|||||||
| `autoMigratePiechartPanel` | Migrate old piechart panel to supported piechart panel - broken out from autoMigrateOldPanels to enable granular tracking |
|
| `autoMigratePiechartPanel` | Migrate old piechart panel to supported piechart panel - broken out from autoMigrateOldPanels to enable granular tracking |
|
||||||
| `autoMigrateWorldmapPanel` | Migrate old worldmap panel to supported geomap panel - broken out from autoMigrateOldPanels to enable granular tracking |
|
| `autoMigrateWorldmapPanel` | Migrate old worldmap panel to supported geomap panel - broken out from autoMigrateOldPanels to enable granular tracking |
|
||||||
| `autoMigrateStatPanel` | Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking |
|
| `autoMigrateStatPanel` | Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking |
|
||||||
|
| `autoMigrateXYChartPanel` | Migrate old XYChart panel to new XYChart2 model |
|
||||||
| `disableAngular` | Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime. |
|
| `disableAngular` | Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime. |
|
||||||
| `newVizTooltips` | New visualizations tooltips UX |
|
| `newVizTooltips` | New visualizations tooltips UX |
|
||||||
| `returnToPrevious` | Enables the return to previous context functionality |
|
| `returnToPrevious` | Enables the return to previous context functionality |
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export * from './DataFrameView';
|
export * from './DataFrameView';
|
||||||
export * from './FieldCache';
|
export * from './FieldCache';
|
||||||
export * from './CircularDataFrame';
|
|
||||||
export * from './MutableDataFrame';
|
export * from './MutableDataFrame';
|
||||||
export * from './processDataFrame';
|
export * from './processDataFrame';
|
||||||
export * from './dimensions';
|
export * from './dimensions';
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { DataFrame, Field, TIME_SERIES_VALUE_FIELD_NAME, FieldType, TIME_SERIES_TIME_FIELD_NAME } from '../types';
|
import { getFieldMatcher } from '../transformations';
|
||||||
|
import {
|
||||||
|
DataFrame,
|
||||||
|
Field,
|
||||||
|
TIME_SERIES_VALUE_FIELD_NAME,
|
||||||
|
FieldType,
|
||||||
|
TIME_SERIES_TIME_FIELD_NAME,
|
||||||
|
FieldConfigSource,
|
||||||
|
} from '../types';
|
||||||
import { formatLabels } from '../utils/labels';
|
import { formatLabels } from '../utils/labels';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,6 +59,53 @@ export function cacheFieldDisplayNames(frames: DataFrame[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* moves each field's config.custom.hideFrom to field.state.hideFrom
|
||||||
|
* and mutates orgiginal field.config.custom.hideFrom to one with explicit overrides only, (without the ad-hoc stateful __system override from legend toggle)
|
||||||
|
*/
|
||||||
|
export function decoupleHideFromState(frames: DataFrame[], fieldConfig: FieldConfigSource<any>) {
|
||||||
|
frames.forEach((frame) => {
|
||||||
|
frame.fields.forEach((field) => {
|
||||||
|
const hideFrom = {
|
||||||
|
legend: false,
|
||||||
|
tooltip: false,
|
||||||
|
viz: false,
|
||||||
|
...fieldConfig.defaults.custom?.hideFrom,
|
||||||
|
};
|
||||||
|
|
||||||
|
// with ad hoc __system override applied
|
||||||
|
const hideFromState = field.config.custom?.hideFrom;
|
||||||
|
|
||||||
|
fieldConfig.overrides.forEach((o) => {
|
||||||
|
if ('__systemRef' in o) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = getFieldMatcher(o.matcher);
|
||||||
|
|
||||||
|
if (m(field, frame, frames)) {
|
||||||
|
for (const p of o.properties) {
|
||||||
|
if (p.id === 'custom.hideFrom') {
|
||||||
|
Object.assign(hideFrom, p.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
field.state = {
|
||||||
|
...field.state,
|
||||||
|
hideFrom: {
|
||||||
|
...hideFromState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// original with perm overrides
|
||||||
|
field.config.custom.hideFrom = hideFrom;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getFieldDisplayName(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string {
|
export function getFieldDisplayName(field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string {
|
||||||
const existingTitle = field.state?.displayName;
|
const existingTitle = field.state?.displayName;
|
||||||
const multipleFrames = Boolean(allFrames && allFrames.length > 1);
|
const multipleFrames = Boolean(allFrames && allFrames.length > 1);
|
||||||
|
|||||||
@@ -48,3 +48,4 @@ export { getLinksSupplier } from './field/fieldOverrides';
|
|||||||
export { CircularVector } from './vector/CircularVector';
|
export { CircularVector } from './vector/CircularVector';
|
||||||
export { vectorator } from './vector/FunctionalVector';
|
export { vectorator } from './vector/FunctionalVector';
|
||||||
export { ArrayVector } from './vector/ArrayVector';
|
export { ArrayVector } from './vector/ArrayVector';
|
||||||
|
export * from './dataframe/CircularDataFrame';
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { HideSeriesConfig } from '@grafana/schema';
|
||||||
|
|
||||||
import { ScopedVars } from './ScopedVars';
|
import { ScopedVars } from './ScopedVars';
|
||||||
import { QueryResultBase, Labels, NullValueMode } from './data';
|
import { QueryResultBase, Labels, NullValueMode } from './data';
|
||||||
import { DataLink, LinkModel } from './dataLink';
|
import { DataLink, LinkModel } from './dataLink';
|
||||||
@@ -231,6 +233,15 @@ export interface FieldState {
|
|||||||
* It's up to each visualization to calculate and set this.
|
* It's up to each visualization to calculate and set this.
|
||||||
*/
|
*/
|
||||||
alignmentFactors?: DisplayValueAlignmentFactors;
|
alignmentFactors?: DisplayValueAlignmentFactors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the current ad-hoc state of whether this series is hidden in viz, tooltip, and legend.
|
||||||
|
*
|
||||||
|
* Currently this will match field.config.custom.hideFrom because fieldOverrides applies the special __system
|
||||||
|
* override to the actual config during toggle via legend. This should go away once we have a unified system
|
||||||
|
* for layering ad hoc field overrides and options but still being able to get the stateless fieldConfig and panel options
|
||||||
|
*/
|
||||||
|
hideFrom?: HideSeriesConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface FeatureToggles {
|
|||||||
autoMigratePiechartPanel?: boolean;
|
autoMigratePiechartPanel?: boolean;
|
||||||
autoMigrateWorldmapPanel?: boolean;
|
autoMigrateWorldmapPanel?: boolean;
|
||||||
autoMigrateStatPanel?: boolean;
|
autoMigrateStatPanel?: boolean;
|
||||||
|
autoMigrateXYChartPanel?: boolean;
|
||||||
disableAngular?: boolean;
|
disableAngular?: boolean;
|
||||||
canvasPanelNesting?: boolean;
|
canvasPanelNesting?: boolean;
|
||||||
newVizTooltips?: boolean;
|
newVizTooltips?: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// Generated by:
|
||||||
|
// public/app/plugins/gen.go
|
||||||
|
// Using jennies:
|
||||||
|
// TSTypesJenny
|
||||||
|
// PluginTsTypesJenny
|
||||||
|
//
|
||||||
|
// Run 'make gen-cue' from repository root to regenerate.
|
||||||
|
|
||||||
|
import * as common from '@grafana/schema';
|
||||||
|
|
||||||
|
export const pluginVersion = "11.0.0-pre";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto is "table" in the UI
|
||||||
|
*/
|
||||||
|
export enum SeriesMapping {
|
||||||
|
Auto = 'auto',
|
||||||
|
Manual = 'manual',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum XYShowMode {
|
||||||
|
Lines = 'lines',
|
||||||
|
Points = 'points',
|
||||||
|
PointsAndLines = 'points+lines',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: (copied from dashboard_kind.cue, since not exported)
|
||||||
|
* Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
|
||||||
|
* It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.
|
||||||
|
*/
|
||||||
|
export interface MatcherConfig {
|
||||||
|
/**
|
||||||
|
* The matcher id. This is used to find the matcher implementation from registry.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The matcher options. This is specific to the matcher implementation.
|
||||||
|
*/
|
||||||
|
options?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultMatcherConfig: Partial<MatcherConfig> = {
|
||||||
|
id: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FieldConfig extends common.HideableFieldConfig, common.AxisConfig {
|
||||||
|
lineStyle?: common.LineStyle;
|
||||||
|
/**
|
||||||
|
* lineColor?: common.ColorDimensionConfig
|
||||||
|
*/
|
||||||
|
lineWidth?: number;
|
||||||
|
pointSize?: {
|
||||||
|
fixed?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
};
|
||||||
|
show?: XYShowMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultFieldConfig: Partial<FieldConfig> = {
|
||||||
|
show: XYShowMode.Points,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface XYSeriesConfig {
|
||||||
|
color?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
frame?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
name?: {
|
||||||
|
fixed?: string;
|
||||||
|
};
|
||||||
|
size?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
x?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
y?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
|
||||||
|
mapping: SeriesMapping;
|
||||||
|
series: Array<XYSeriesConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOptions: Partial<Options> = {
|
||||||
|
series: [],
|
||||||
|
};
|
||||||
@@ -72,7 +72,7 @@ export function VizLegend<T>({
|
|||||||
}
|
}
|
||||||
if (onToggleSeriesVisibility) {
|
if (onToggleSeriesVisibility) {
|
||||||
onToggleSeriesVisibility(
|
onToggleSeriesVisibility(
|
||||||
item.label,
|
item.fieldName ?? item.label,
|
||||||
seriesVisibilityChangeBehavior === SeriesVisibilityChangeBehavior.Hide
|
seriesVisibilityChangeBehavior === SeriesVisibilityChangeBehavior.Hide
|
||||||
? SeriesVisibilityChangeMode.AppendToSelection
|
? SeriesVisibilityChangeMode.AppendToSelection
|
||||||
: mapMouseEventToMode(event)
|
: mapMouseEventToMode(event)
|
||||||
|
|||||||
@@ -70,7 +70,12 @@ export const VizLegendListItem = <T = unknown,>({
|
|||||||
className={cx(styles.itemWrapper, item.disabled && styles.itemDisabled, className)}
|
className={cx(styles.itemWrapper, item.disabled && styles.itemDisabled, className)}
|
||||||
data-testid={selectors.components.VizLegend.seriesName(item.label)}
|
data-testid={selectors.components.VizLegend.seriesName(item.label)}
|
||||||
>
|
>
|
||||||
<VizLegendSeriesIcon seriesName={item.label} color={item.color} gradient={item.gradient} readonly={readonly} />
|
<VizLegendSeriesIcon
|
||||||
|
seriesName={item.fieldName ?? item.label}
|
||||||
|
color={item.color}
|
||||||
|
gradient={item.gradient}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const LegendTableItem = ({
|
|||||||
<tr className={cx(styles.row, className)}>
|
<tr className={cx(styles.row, className)}>
|
||||||
<td>
|
<td>
|
||||||
<span className={styles.itemWrapper}>
|
<span className={styles.itemWrapper}>
|
||||||
<VizLegendSeriesIcon color={item.color} seriesName={item.label} readonly={readonly} />
|
<VizLegendSeriesIcon color={item.color} seriesName={item.fieldName ?? item.label} readonly={readonly} />
|
||||||
<button
|
<button
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -47,5 +47,6 @@ export interface VizLegendItem<T = any> {
|
|||||||
// displayValues?: DisplayValue[];
|
// displayValues?: DisplayValue[];
|
||||||
getDisplayValues?: () => DisplayValue[];
|
getDisplayValues?: () => DisplayValue[];
|
||||||
fieldIndex?: DataFrameFieldIndex;
|
fieldIndex?: DataFrameFieldIndex;
|
||||||
|
fieldName?: string;
|
||||||
data?: T;
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,13 @@ var (
|
|||||||
FrontendOnly: true,
|
FrontendOnly: true,
|
||||||
Owner: grafanaDatavizSquad,
|
Owner: grafanaDatavizSquad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "autoMigrateXYChartPanel",
|
||||||
|
Description: "Migrate old XYChart panel to new XYChart2 model",
|
||||||
|
Stage: FeatureStagePublicPreview,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Owner: grafanaDatavizSquad,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "disableAngular",
|
Name: "disableAngular",
|
||||||
Description: "Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.",
|
Description: "Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ autoMigrateTablePanel,preview,@grafana/dataviz-squad,false,false,true
|
|||||||
autoMigratePiechartPanel,preview,@grafana/dataviz-squad,false,false,true
|
autoMigratePiechartPanel,preview,@grafana/dataviz-squad,false,false,true
|
||||||
autoMigrateWorldmapPanel,preview,@grafana/dataviz-squad,false,false,true
|
autoMigrateWorldmapPanel,preview,@grafana/dataviz-squad,false,false,true
|
||||||
autoMigrateStatPanel,preview,@grafana/dataviz-squad,false,false,true
|
autoMigrateStatPanel,preview,@grafana/dataviz-squad,false,false,true
|
||||||
|
autoMigrateXYChartPanel,preview,@grafana/dataviz-squad,false,false,true
|
||||||
disableAngular,preview,@grafana/dataviz-squad,false,false,true
|
disableAngular,preview,@grafana/dataviz-squad,false,false,true
|
||||||
canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true
|
canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true
|
||||||
newVizTooltips,preview,@grafana/dataviz-squad,false,false,true
|
newVizTooltips,preview,@grafana/dataviz-squad,false,false,true
|
||||||
|
|||||||
|
@@ -87,6 +87,10 @@ const (
|
|||||||
// Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking
|
// Migrate old stat panel to supported stat panel - broken out from autoMigrateOldPanels to enable granular tracking
|
||||||
FlagAutoMigrateStatPanel = "autoMigrateStatPanel"
|
FlagAutoMigrateStatPanel = "autoMigrateStatPanel"
|
||||||
|
|
||||||
|
// FlagAutoMigrateXYChartPanel
|
||||||
|
// Migrate old XYChart panel to new XYChart2 model
|
||||||
|
FlagAutoMigrateXYChartPanel = "autoMigrateXYChartPanel"
|
||||||
|
|
||||||
// FlagDisableAngular
|
// FlagDisableAngular
|
||||||
// Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.
|
// Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.
|
||||||
FlagDisableAngular = "disableAngular"
|
FlagDisableAngular = "disableAngular"
|
||||||
|
|||||||
@@ -2177,6 +2177,19 @@
|
|||||||
"codeowner": "@grafana/sharing-squad",
|
"codeowner": "@grafana/sharing-squad",
|
||||||
"frontend": true
|
"frontend": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "autoMigrateXYChartPanel",
|
||||||
|
"resourceVersion": "1711119444221",
|
||||||
|
"creationTimestamp": "2024-03-22T14:57:24Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Migrate old XYChart panel to new XYChart2 model",
|
||||||
|
"stage": "preview",
|
||||||
|
"codeowner": "@grafana/dataviz-squad",
|
||||||
|
"frontend": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1403,7 +1403,7 @@ function upgradeValueMappings(oldMappings: any, thresholds?: ThresholdsConfig):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrateTooltipOptions(panel: PanelModel) {
|
function migrateTooltipOptions(panel: PanelModel) {
|
||||||
if (panel.type === 'timeseries' || panel.type === 'xychart') {
|
if (panel.type === 'timeseries' || panel.type === 'xychart' || panel.type === 'xychart2') {
|
||||||
if (panel.options.tooltipOptions) {
|
if (panel.options.tooltipOptions) {
|
||||||
panel.options = {
|
panel.options = {
|
||||||
...panel.options,
|
...panel.options,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const mssqlPlugin = async () =>
|
|||||||
const alertmanagerPlugin = async () =>
|
const alertmanagerPlugin = async () =>
|
||||||
await import(/* webpackChunkName: "alertmanagerPlugin" */ 'app/plugins/datasource/alertmanager/module');
|
await import(/* webpackChunkName: "alertmanagerPlugin" */ 'app/plugins/datasource/alertmanager/module');
|
||||||
|
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
|
import * as alertListPanel from 'app/plugins/panel/alertlist/module';
|
||||||
import * as annoListPanel from 'app/plugins/panel/annolist/module';
|
import * as annoListPanel from 'app/plugins/panel/annolist/module';
|
||||||
import * as barChartPanel from 'app/plugins/panel/barchart/module';
|
import * as barChartPanel from 'app/plugins/panel/barchart/module';
|
||||||
@@ -51,12 +52,18 @@ import * as timeseriesPanel from 'app/plugins/panel/timeseries/module';
|
|||||||
import * as tracesPanel from 'app/plugins/panel/traces/module';
|
import * as tracesPanel from 'app/plugins/panel/traces/module';
|
||||||
import * as trendPanel from 'app/plugins/panel/trend/module';
|
import * as trendPanel from 'app/plugins/panel/trend/module';
|
||||||
import * as welcomeBanner from 'app/plugins/panel/welcome/module';
|
import * as welcomeBanner from 'app/plugins/panel/welcome/module';
|
||||||
import * as xyChartPanel from 'app/plugins/panel/xychart/module';
|
|
||||||
|
|
||||||
// Async loaded panels
|
// Async loaded panels
|
||||||
const geomapPanel = async () => await import(/* webpackChunkName: "geomapPanel" */ 'app/plugins/panel/geomap/module');
|
const geomapPanel = async () => await import(/* webpackChunkName: "geomapPanel" */ 'app/plugins/panel/geomap/module');
|
||||||
const canvasPanel = async () => await import(/* webpackChunkName: "canvasPanel" */ 'app/plugins/panel/canvas/module');
|
const canvasPanel = async () => await import(/* webpackChunkName: "canvasPanel" */ 'app/plugins/panel/canvas/module');
|
||||||
const graphPanel = async () => await import(/* webpackChunkName: "graphPlugin" */ 'app/plugins/panel/graph/module');
|
const graphPanel = async () => await import(/* webpackChunkName: "graphPlugin" */ 'app/plugins/panel/graph/module');
|
||||||
|
const xychartPanel = async () => {
|
||||||
|
if (config.featureToggles.autoMigrateXYChartPanel) {
|
||||||
|
return await import(/* webpackChunkName: "xychart2" */ 'app/plugins/panel/xychart/v2/module');
|
||||||
|
} else {
|
||||||
|
return await import(/* webpackChunkName: "xychart" */ 'app/plugins/panel/xychart/module');
|
||||||
|
}
|
||||||
|
};
|
||||||
const heatmapPanel = async () =>
|
const heatmapPanel = async () =>
|
||||||
await import(/* webpackChunkName: "heatmapPanel" */ 'app/plugins/panel/heatmap/module');
|
await import(/* webpackChunkName: "heatmapPanel" */ 'app/plugins/panel/heatmap/module');
|
||||||
const tableOldPanel = async () =>
|
const tableOldPanel = async () =>
|
||||||
@@ -89,7 +96,7 @@ const builtInPlugins: Record<string, System.Module | (() => Promise<System.Modul
|
|||||||
'core:plugin/status-history': statusHistoryPanel,
|
'core:plugin/status-history': statusHistoryPanel,
|
||||||
'core:plugin/candlestick': candlestickPanel,
|
'core:plugin/candlestick': candlestickPanel,
|
||||||
'core:plugin/graph': graphPanel,
|
'core:plugin/graph': graphPanel,
|
||||||
'core:plugin/xychart': xyChartPanel,
|
'core:plugin/xychart': xychartPanel,
|
||||||
'core:plugin/geomap': geomapPanel,
|
'core:plugin/geomap': geomapPanel,
|
||||||
'core:plugin/canvas': canvasPanel,
|
'core:plugin/canvas': canvasPanel,
|
||||||
'core:plugin/dashlist': dashListPanel,
|
'core:plugin/dashlist': dashListPanel,
|
||||||
|
|||||||
3
public/app/plugins/panel/xychart/v2/README.md
Normal file
3
public/app/plugins/panel/xychart/v2/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# XY Chart - Native Plugin
|
||||||
|
|
||||||
|
Support arbitrary X vs Y in graph
|
||||||
326
public/app/plugins/panel/xychart/v2/SeriesEditor.tsx
Normal file
326
public/app/plugins/panel/xychart/v2/SeriesEditor.tsx
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import React, { Fragment, useState } from 'react';
|
||||||
|
import { usePrevious } from 'react-use';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getFrameDisplayName,
|
||||||
|
StandardEditorProps,
|
||||||
|
// getFieldDisplayName,
|
||||||
|
FrameMatcherID,
|
||||||
|
FieldMatcherID,
|
||||||
|
FieldNamePickerBaseNameMode,
|
||||||
|
FieldType,
|
||||||
|
GrafanaTheme2,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { Button, Field, IconButton, Select, useStyles2 } from '@grafana/ui';
|
||||||
|
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
||||||
|
import { LayerName } from 'app/core/components/Layers/LayerName';
|
||||||
|
|
||||||
|
import { Options, SeriesMapping, XYSeriesConfig } from './panelcfg.gen';
|
||||||
|
|
||||||
|
export const SeriesEditor = ({
|
||||||
|
value: seriesCfg,
|
||||||
|
onChange,
|
||||||
|
context,
|
||||||
|
}: StandardEditorProps<XYSeriesConfig[], unknown, Options>) => {
|
||||||
|
const style = useStyles2(getStyles);
|
||||||
|
|
||||||
|
// reset opts when mapping changes (no way to do this in panel opts builder?)
|
||||||
|
const mapping = context.options?.mapping as SeriesMapping;
|
||||||
|
const prevMapping = usePrevious(mapping);
|
||||||
|
const mappingChanged = prevMapping != null && mapping !== prevMapping;
|
||||||
|
|
||||||
|
if (mappingChanged || seriesCfg == null || seriesCfg.length === 0) {
|
||||||
|
seriesCfg = [{}];
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [selectedIdx, setSelectedIdx] = useState(0);
|
||||||
|
|
||||||
|
const addSeries = () => {
|
||||||
|
seriesCfg = seriesCfg.concat({});
|
||||||
|
setSelectedIdx(seriesCfg.length - 1);
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSeries = (index: number) => {
|
||||||
|
seriesCfg = seriesCfg.filter((s, i) => i !== index);
|
||||||
|
setSelectedIdx(0);
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const series = seriesCfg[selectedIdx];
|
||||||
|
|
||||||
|
const formKey = `${mapping}${selectedIdx}`;
|
||||||
|
|
||||||
|
const baseNameMode =
|
||||||
|
mapping === SeriesMapping.Manual
|
||||||
|
? FieldNamePickerBaseNameMode.ExcludeBaseNames
|
||||||
|
: context.data.length === 1
|
||||||
|
? FieldNamePickerBaseNameMode.IncludeAll
|
||||||
|
: FieldNamePickerBaseNameMode.OnlyBaseNames;
|
||||||
|
|
||||||
|
context.data.forEach((frame, frameIndex) => {
|
||||||
|
frame.fields.forEach((field, fieldIndex) => {
|
||||||
|
field.state = {
|
||||||
|
...field.state,
|
||||||
|
origin: {
|
||||||
|
frameIndex,
|
||||||
|
fieldIndex,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mapping === SeriesMapping.Manual && (
|
||||||
|
<>
|
||||||
|
<Button icon="plus" size="sm" variant="secondary" onClick={addSeries} className={style.marginBot}>
|
||||||
|
Add series
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className={style.marginBot}>
|
||||||
|
{seriesCfg.map((series, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`series/${index}`}
|
||||||
|
className={index === selectedIdx ? `${style.row} ${style.sel}` : style.row}
|
||||||
|
onClick={() => setSelectedIdx(index)}
|
||||||
|
role="button"
|
||||||
|
aria-label={`Select series ${index + 1}`}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
setSelectedIdx(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LayerName
|
||||||
|
name={series.name?.fixed ?? `Series ${index + 1}`}
|
||||||
|
onChange={(v) => {
|
||||||
|
series.name = {
|
||||||
|
fixed: v === '' || v === `Series ${index + 1}` ? undefined : v,
|
||||||
|
};
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
name="trash-alt"
|
||||||
|
title={'remove'}
|
||||||
|
className={cx(style.actionIcon)}
|
||||||
|
onClick={() => deleteSeries(index)}
|
||||||
|
tooltip="Delete series"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Fragment key={formKey}>
|
||||||
|
<Field label="Frame">
|
||||||
|
<Select
|
||||||
|
placeholder={mapping === SeriesMapping.Auto ? 'All frames' : 'Select frame'}
|
||||||
|
isClearable={true}
|
||||||
|
options={context.data.map((frame, index) => ({
|
||||||
|
value: index,
|
||||||
|
label: `${getFrameDisplayName(frame, index)} (index: ${index}, rows: ${frame.length})`,
|
||||||
|
}))}
|
||||||
|
value={series.frame?.matcher.options}
|
||||||
|
onChange={(opt) => {
|
||||||
|
if (opt == null) {
|
||||||
|
delete series.frame;
|
||||||
|
} else {
|
||||||
|
series.frame = {
|
||||||
|
matcher: {
|
||||||
|
id: FrameMatcherID.byIndex,
|
||||||
|
options: Number(opt.value),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="X field">
|
||||||
|
<FieldNamePicker
|
||||||
|
value={series.x?.matcher.options as string}
|
||||||
|
context={context}
|
||||||
|
onChange={(fieldName) => {
|
||||||
|
if (fieldName == null) {
|
||||||
|
delete series.x;
|
||||||
|
} else {
|
||||||
|
// TODO: reset any other dim that was set to fieldName
|
||||||
|
series.x = {
|
||||||
|
matcher: {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: fieldName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
}}
|
||||||
|
item={{
|
||||||
|
id: 'x',
|
||||||
|
name: 'x',
|
||||||
|
settings: {
|
||||||
|
filter: (field) =>
|
||||||
|
(mapping === SeriesMapping.Auto ||
|
||||||
|
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
|
||||||
|
field.type === FieldType.number &&
|
||||||
|
!field.config.custom?.hideFrom?.viz,
|
||||||
|
baseNameMode,
|
||||||
|
placeholderText: mapping === SeriesMapping.Auto ? 'First number field in each frame' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Y field">
|
||||||
|
<FieldNamePicker
|
||||||
|
value={series.y?.matcher?.options as string}
|
||||||
|
context={context}
|
||||||
|
onChange={(fieldName) => {
|
||||||
|
if (fieldName == null) {
|
||||||
|
delete series.y;
|
||||||
|
} else {
|
||||||
|
// TODO: reset any other dim that was set to fieldName
|
||||||
|
series.y = {
|
||||||
|
matcher: {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: fieldName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
}}
|
||||||
|
item={{
|
||||||
|
id: 'y',
|
||||||
|
name: 'y',
|
||||||
|
settings: {
|
||||||
|
// TODO: filter out series.y?.exclude.options, series.size.matcher.options, series.color.matcher.options
|
||||||
|
filter: (field) =>
|
||||||
|
(mapping === SeriesMapping.Auto ||
|
||||||
|
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
|
||||||
|
field.type === FieldType.number &&
|
||||||
|
!field.config.custom?.hideFrom?.viz,
|
||||||
|
baseNameMode,
|
||||||
|
placeholderText: mapping === SeriesMapping.Auto ? 'Remaining number fields in each frame' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Size field">
|
||||||
|
<FieldNamePicker
|
||||||
|
value={series.size?.matcher?.options as string}
|
||||||
|
context={context}
|
||||||
|
onChange={(fieldName) => {
|
||||||
|
if (fieldName == null) {
|
||||||
|
delete series.size;
|
||||||
|
} else {
|
||||||
|
// TODO: reset any other dim that was set to fieldName
|
||||||
|
series.size = {
|
||||||
|
matcher: {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: fieldName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
}}
|
||||||
|
item={{
|
||||||
|
id: 'size',
|
||||||
|
name: 'size',
|
||||||
|
settings: {
|
||||||
|
// TODO: filter out series.y?.exclude.options, series.size.matcher.options, series.color.matcher.options
|
||||||
|
filter: (field) =>
|
||||||
|
field.name !== series.x?.matcher.options &&
|
||||||
|
(mapping === SeriesMapping.Auto ||
|
||||||
|
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
|
||||||
|
field.type === FieldType.number &&
|
||||||
|
!field.config.custom?.hideFrom?.viz,
|
||||||
|
baseNameMode,
|
||||||
|
placeholderText: '',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="Color field">
|
||||||
|
<FieldNamePicker
|
||||||
|
value={series.color?.matcher?.options as string}
|
||||||
|
context={context}
|
||||||
|
onChange={(fieldName) => {
|
||||||
|
if (fieldName == null) {
|
||||||
|
delete series.color;
|
||||||
|
} else {
|
||||||
|
// TODO: reset any other dim that was set to fieldName
|
||||||
|
series.color = {
|
||||||
|
matcher: {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: fieldName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange([...seriesCfg]);
|
||||||
|
}}
|
||||||
|
item={{
|
||||||
|
id: 'color',
|
||||||
|
name: 'color',
|
||||||
|
settings: {
|
||||||
|
// TODO: filter out series.y?.exclude.options, series.size.matcher.options, series.color.matcher.options
|
||||||
|
filter: (field) =>
|
||||||
|
field.name !== series.x?.matcher.options &&
|
||||||
|
(mapping === SeriesMapping.Auto ||
|
||||||
|
field.state?.origin?.frameIndex === series.frame?.matcher.options) &&
|
||||||
|
field.type === FieldType.number &&
|
||||||
|
!field.config.custom?.hideFrom?.viz,
|
||||||
|
baseNameMode,
|
||||||
|
placeholderText: '',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Fragment>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
marginBot: css({
|
||||||
|
marginBottom: '20px',
|
||||||
|
}),
|
||||||
|
row: css({
|
||||||
|
padding: `${theme.spacing(0.5, 1)}`,
|
||||||
|
borderRadius: `${theme.shape.radius.default}`,
|
||||||
|
background: `${theme.colors.background.secondary}`,
|
||||||
|
minHeight: `${theme.spacing(4)}`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: '3px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
|
||||||
|
border: `1px solid ${theme.components.input.borderColor}`,
|
||||||
|
'&:hover': {
|
||||||
|
border: `1px solid ${theme.components.input.borderHover}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
sel: css({
|
||||||
|
border: `1px solid ${theme.colors.primary.border}`,
|
||||||
|
'&:hover': {
|
||||||
|
border: `1px solid ${theme.colors.primary.border}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
actionIcon: css({
|
||||||
|
color: `${theme.colors.text.secondary}`,
|
||||||
|
'&:hover': {
|
||||||
|
color: `${theme.colors.text}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
130
public/app/plugins/panel/xychart/v2/XYChartPanel.tsx
Normal file
130
public/app/plugins/panel/xychart/v2/XYChartPanel.tsx
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { PanelProps } from '@grafana/data';
|
||||||
|
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import {
|
||||||
|
TooltipDisplayMode,
|
||||||
|
TooltipPlugin2,
|
||||||
|
UPlotChart,
|
||||||
|
VizLayout,
|
||||||
|
VizLegend,
|
||||||
|
VizLegendItem,
|
||||||
|
useStyles2,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
|
||||||
|
|
||||||
|
import { XYChartTooltip } from './XYChartTooltip';
|
||||||
|
import { Options } from './panelcfg.gen';
|
||||||
|
import { prepConfig } from './scatter';
|
||||||
|
import { prepSeries } from './utils';
|
||||||
|
|
||||||
|
type Props2 = PanelProps<Options>;
|
||||||
|
|
||||||
|
export const XYChartPanel2 = (props: Props2) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
let { mapping, series: mappedSeries } = props.options;
|
||||||
|
|
||||||
|
// regenerate series schema when mappings or data changes
|
||||||
|
let series = useMemo(
|
||||||
|
() => prepSeries(mapping, mappedSeries, props.data.series, props.fieldConfig),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[mapping, mappedSeries, props.data.series, props.fieldConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
// if series changed due to mappings or data structure, re-init config & renderers
|
||||||
|
let { builder, prepData } = useMemo(
|
||||||
|
() => prepConfig(series, config.theme2),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[mapping, mappedSeries, props.data.structureRev, props.fieldConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
// generate data struct for uPlot mode: 2
|
||||||
|
let data = useMemo(
|
||||||
|
() => prepData(series),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[series]
|
||||||
|
);
|
||||||
|
|
||||||
|
// todo: handle errors
|
||||||
|
let error = builder == null || data.length === 0 ? 'Err' : '';
|
||||||
|
|
||||||
|
// TODO: React.memo()
|
||||||
|
const renderLegend = () => {
|
||||||
|
const items: VizLegendItem[] = [];
|
||||||
|
|
||||||
|
series.forEach((s, idx) => {
|
||||||
|
let yField = s.y.field;
|
||||||
|
let config = yField.config;
|
||||||
|
let custom = config.custom;
|
||||||
|
|
||||||
|
if (!custom.hideFrom?.legend) {
|
||||||
|
items.push({
|
||||||
|
yAxis: 1, // TODO: pull from y field
|
||||||
|
label: s.name.value,
|
||||||
|
color: alpha(s.color.fixed!, 1),
|
||||||
|
getItemKey: () => `${idx}-${s.name.value}`,
|
||||||
|
fieldName: yField.state?.displayName ?? yField.name,
|
||||||
|
disabled: yField.state?.hideFrom?.viz ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort series by calcs? table mode?
|
||||||
|
|
||||||
|
const { placement, displayMode, width } = props.options.legend;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VizLayout.Legend placement={placement} width={width}>
|
||||||
|
<VizLegend className={styles.legend} placement={placement} items={items} displayMode={displayMode} />
|
||||||
|
</VizLayout.Legend>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<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={data} width={vizWidth} height={vizHeight}>
|
||||||
|
{props.options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||||
|
<TooltipPlugin2
|
||||||
|
config={builder!}
|
||||||
|
hoverMode={TooltipHoverMode.xyOne}
|
||||||
|
render={(u, dataIdxs, seriesIdx, isPinned, dismiss) => {
|
||||||
|
return (
|
||||||
|
<XYChartTooltip
|
||||||
|
data={props.data.series}
|
||||||
|
dataIdxs={dataIdxs}
|
||||||
|
xySeries={series}
|
||||||
|
dismiss={dismiss}
|
||||||
|
isPinned={isPinned}
|
||||||
|
seriesIdx={seriesIdx!}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
maxWidth={props.options.tooltip.maxWidth}
|
||||||
|
maxHeight={props.options.tooltip.maxHeight}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</UPlotChart>
|
||||||
|
)}
|
||||||
|
</VizLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = () => ({
|
||||||
|
legend: css({
|
||||||
|
div: {
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
85
public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx
Normal file
85
public/app/plugins/panel/xychart/v2/XYChartTooltip.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { DataFrame } from '@grafana/data';
|
||||||
|
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||||
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
|
||||||
|
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
|
||||||
|
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
|
||||||
|
import { ColorIndicator, VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types';
|
||||||
|
|
||||||
|
import { getDataLinks } from '../../status-history/utils';
|
||||||
|
import { getStyles } from '../../timeseries/TimeSeriesTooltip';
|
||||||
|
|
||||||
|
import { XYSeries } from './types2';
|
||||||
|
import { fmt } from './utils';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
dataIdxs: Array<number | null>;
|
||||||
|
seriesIdx: number | null | undefined;
|
||||||
|
isPinned: boolean;
|
||||||
|
dismiss: () => void;
|
||||||
|
data: DataFrame[];
|
||||||
|
xySeries: XYSeries[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, isPinned }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const rowIndex = dataIdxs.find((idx) => idx !== null)!;
|
||||||
|
|
||||||
|
const series = xySeries[seriesIdx! - 1];
|
||||||
|
const xField = series.x.field;
|
||||||
|
const yField = series.y.field;
|
||||||
|
|
||||||
|
let label = series.name.value;
|
||||||
|
|
||||||
|
let seriesColor = series.color.fixed;
|
||||||
|
// let colorField = series.color.field;
|
||||||
|
// let pointColor: string;
|
||||||
|
|
||||||
|
// if (colorField != null) {
|
||||||
|
// pointColor = colorField.display?.(colorField.values[rowIndex]).color!;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const headerItem: VizTooltipItem = {
|
||||||
|
label,
|
||||||
|
value: '',
|
||||||
|
color: alpha(seriesColor!, 0.5),
|
||||||
|
colorIndicator: ColorIndicator.marker_md,
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentItems: VizTooltipItem[] = [
|
||||||
|
{
|
||||||
|
label: xField.state?.displayName ?? xField.name,
|
||||||
|
value: fmt(xField, xField.values[rowIndex]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: yField.state?.displayName ?? yField.name,
|
||||||
|
value: fmt(yField, yField.values[rowIndex]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
series._rest.forEach((field) => {
|
||||||
|
contentItems.push({
|
||||||
|
label: field.state?.displayName ?? field.name,
|
||||||
|
value: fmt(field, field.values[rowIndex]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let footer: ReactNode;
|
||||||
|
|
||||||
|
if (isPinned && seriesIdx != null) {
|
||||||
|
const links = getDataLinks(yField, rowIndex);
|
||||||
|
|
||||||
|
footer = <VizTooltipFooter dataLinks={links} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<VizTooltipHeader item={headerItem} isPinned={isPinned} />
|
||||||
|
<VizTooltipContent items={contentItems} isPinned={isPinned} />
|
||||||
|
{footer}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
151
public/app/plugins/panel/xychart/v2/config.ts
Normal file
151
public/app/plugins/panel/xychart/v2/config.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import {
|
||||||
|
FieldColorModeId,
|
||||||
|
FieldConfigProperty,
|
||||||
|
FieldType,
|
||||||
|
identityOverrideProcessor,
|
||||||
|
SetFieldConfigOptionsArgs,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { LineStyle } from '@grafana/schema';
|
||||||
|
import { commonOptionsBuilder } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { LineStyleEditor } from '../../timeseries/LineStyleEditor';
|
||||||
|
|
||||||
|
import { FieldConfig, XYShowMode } from './panelcfg.gen';
|
||||||
|
|
||||||
|
export const DEFAULT_POINT_SIZE = 5;
|
||||||
|
|
||||||
|
export function getScatterFieldConfig(cfg: FieldConfig): SetFieldConfigOptionsArgs<FieldConfig> {
|
||||||
|
return {
|
||||||
|
standardOptions: {
|
||||||
|
[FieldConfigProperty.Min]: {
|
||||||
|
hideFromDefaults: true,
|
||||||
|
},
|
||||||
|
[FieldConfigProperty.Max]: {
|
||||||
|
hideFromDefaults: true,
|
||||||
|
},
|
||||||
|
[FieldConfigProperty.Unit]: {
|
||||||
|
hideFromDefaults: true,
|
||||||
|
},
|
||||||
|
[FieldConfigProperty.Decimals]: {
|
||||||
|
hideFromDefaults: true,
|
||||||
|
},
|
||||||
|
[FieldConfigProperty.NoValue]: {
|
||||||
|
hideFromDefaults: true,
|
||||||
|
},
|
||||||
|
[FieldConfigProperty.DisplayName]: {
|
||||||
|
hideFromDefaults: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
[FieldConfigProperty.Thresholds]: {
|
||||||
|
hideFromDefaults: true,
|
||||||
|
},
|
||||||
|
[FieldConfigProperty.Mappings]: {
|
||||||
|
hideFromDefaults: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: this still leaves Color series by: [ Last | Min | Max ]
|
||||||
|
// because item.settings?.bySeriesSupport && colorMode.isByValue
|
||||||
|
[FieldConfigProperty.Color]: {
|
||||||
|
settings: {
|
||||||
|
byValueSupport: true,
|
||||||
|
bySeriesSupport: true,
|
||||||
|
preferThresholdsMode: false,
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
mode: FieldColorModeId.PaletteClassic,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
useCustomConfig: (builder) => {
|
||||||
|
builder
|
||||||
|
.addRadio({
|
||||||
|
path: 'show',
|
||||||
|
name: 'Show',
|
||||||
|
defaultValue: cfg.show,
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ label: 'Points', value: XYShowMode.Points },
|
||||||
|
{ label: 'Lines', value: XYShowMode.Lines },
|
||||||
|
{ label: 'Both', value: XYShowMode.PointsAndLines },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// .addGenericEditor(
|
||||||
|
// {
|
||||||
|
// path: 'pointSymbol',
|
||||||
|
// name: 'Point symbol',
|
||||||
|
// defaultValue: defaultFieldConfig.pointSymbol ?? {
|
||||||
|
// mode: 'fixed',
|
||||||
|
// fixed: 'img/icons/marker/circle.svg',
|
||||||
|
// },
|
||||||
|
// settings: {
|
||||||
|
// resourceType: MediaType.Icon,
|
||||||
|
// folderName: ResourceFolderName.Marker,
|
||||||
|
// placeholderText: 'Select a symbol',
|
||||||
|
// placeholderValue: 'img/icons/marker/circle.svg',
|
||||||
|
// showSourceRadio: false,
|
||||||
|
// },
|
||||||
|
// showIf: (c) => c.show !== ScatterShow.Lines,
|
||||||
|
// },
|
||||||
|
// SymbolEditor // ResourceDimensionEditor
|
||||||
|
// )
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'pointSize.fixed',
|
||||||
|
name: 'Point size',
|
||||||
|
defaultValue: cfg.pointSize?.fixed ?? DEFAULT_POINT_SIZE,
|
||||||
|
settings: {
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
showIf: (c) => c.show !== XYShowMode.Lines,
|
||||||
|
})
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'pointSize.min',
|
||||||
|
name: 'Min point size',
|
||||||
|
showIf: (c) => c.show !== XYShowMode.Lines,
|
||||||
|
})
|
||||||
|
.addNumberInput({
|
||||||
|
path: 'pointSize.max',
|
||||||
|
name: 'Max point size',
|
||||||
|
showIf: (c) => c.show !== XYShowMode.Lines,
|
||||||
|
})
|
||||||
|
// .addSliderInput({
|
||||||
|
// path: 'fillOpacity',
|
||||||
|
// name: 'Fill opacity',
|
||||||
|
// defaultValue: 0.4, // defaultFieldConfig.fillOpacity,
|
||||||
|
// settings: {
|
||||||
|
// min: 0, // hidden? or just outlines?
|
||||||
|
// max: 1,
|
||||||
|
// step: 0.05,
|
||||||
|
// },
|
||||||
|
// showIf: (c) => c.show !== ScatterShow.Lines,
|
||||||
|
// })
|
||||||
|
.addCustomEditor<void, LineStyle>({
|
||||||
|
id: 'lineStyle',
|
||||||
|
path: 'lineStyle',
|
||||||
|
name: 'Line style',
|
||||||
|
showIf: (c) => c.show !== XYShowMode.Points,
|
||||||
|
editor: LineStyleEditor,
|
||||||
|
override: LineStyleEditor,
|
||||||
|
process: identityOverrideProcessor,
|
||||||
|
shouldApply: (f) => f.type === FieldType.number,
|
||||||
|
})
|
||||||
|
.addSliderInput({
|
||||||
|
path: 'lineWidth',
|
||||||
|
name: 'Line width',
|
||||||
|
defaultValue: cfg.lineWidth,
|
||||||
|
settings: {
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
showIf: (c) => c.show !== XYShowMode.Points,
|
||||||
|
});
|
||||||
|
|
||||||
|
commonOptionsBuilder.addAxisConfig(builder, cfg);
|
||||||
|
commonOptionsBuilder.addHideFrom(builder);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
149
public/app/plugins/panel/xychart/v2/migrations.test.ts
Normal file
149
public/app/plugins/panel/xychart/v2/migrations.test.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { PanelModel } from '@grafana/data';
|
||||||
|
|
||||||
|
import { xyChartMigrationHandler } from './migrations';
|
||||||
|
import { Options } from './panelcfg.gen';
|
||||||
|
|
||||||
|
describe('XYChart migrations', () => {
|
||||||
|
it('keep import', () => {
|
||||||
|
let input = { series: [] } as unknown as Options;
|
||||||
|
const options = xyChartMigrationHandler({
|
||||||
|
pluginVersion: 'x.y.z', // when defined
|
||||||
|
options: input,
|
||||||
|
} as PanelModel);
|
||||||
|
|
||||||
|
// no changes
|
||||||
|
expect(options).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
it('should migrate to new format for GA 10.4 release', () => {
|
||||||
|
const panel = {
|
||||||
|
options: {
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
x: 'x',
|
||||||
|
y: 'y',
|
||||||
|
size: {
|
||||||
|
fixed: 10,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
fixed: 'red',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as PanelModel;
|
||||||
|
|
||||||
|
const options = xyChartMigrationHandler(panel);
|
||||||
|
expect(options.series![0].x).toEqual({
|
||||||
|
field: {
|
||||||
|
matcher: {
|
||||||
|
id: 'byName',
|
||||||
|
options: 'x',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(options.series![0].y).toEqual({
|
||||||
|
field: {
|
||||||
|
matcher: {
|
||||||
|
id: 'byName',
|
||||||
|
options: 'y',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update these `as any` when types are settled
|
||||||
|
expect((options.series![0] as any).size).toEqual({
|
||||||
|
fixed: 10,
|
||||||
|
});
|
||||||
|
expect((options.series![0] as any).color).toEqual({
|
||||||
|
fixed: 'red',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// test to make sure that migration does not run if the plugin version is not empty
|
||||||
|
it('should not run migration if plugin version is not empty', () => {
|
||||||
|
const panel = {
|
||||||
|
pluginVersion: '10.4.0',
|
||||||
|
// Old options
|
||||||
|
options: {
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
x: 'x',
|
||||||
|
y: 'y',
|
||||||
|
size: {
|
||||||
|
fixed: 10,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
fixed: 'red',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as PanelModel;
|
||||||
|
|
||||||
|
const options = xyChartMigrationHandler(panel);
|
||||||
|
expect(options).toEqual(panel.options);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Include y exclude fields as well as field matchers for size and color
|
||||||
|
it('should include y exclude fields as well as field matchers for size and color', () => {
|
||||||
|
const panel = {
|
||||||
|
options: {
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
x: 'x',
|
||||||
|
y: 'y',
|
||||||
|
size: {
|
||||||
|
fixed: 10,
|
||||||
|
field: 'size',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
fixed: 'red',
|
||||||
|
field: 'color',
|
||||||
|
},
|
||||||
|
dims: {
|
||||||
|
exclude: ['y1', 'y2'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as PanelModel;
|
||||||
|
|
||||||
|
const options = xyChartMigrationHandler(panel);
|
||||||
|
expect(options.series![0].y).toEqual({
|
||||||
|
field: {
|
||||||
|
matcher: {
|
||||||
|
id: 'byName',
|
||||||
|
options: 'y',
|
||||||
|
},
|
||||||
|
exclude: {
|
||||||
|
id: 'byNames',
|
||||||
|
options: ['y1', 'y2'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update these `as any` when types are settled
|
||||||
|
expect((options.series![0] as any).size).toEqual({
|
||||||
|
fixed: 10,
|
||||||
|
field: {
|
||||||
|
matcher: {
|
||||||
|
id: 'byName',
|
||||||
|
options: 'size',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect((options.series![0] as any).color).toEqual({
|
||||||
|
fixed: 'red',
|
||||||
|
field: {
|
||||||
|
matcher: {
|
||||||
|
id: 'byName',
|
||||||
|
options: 'color',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
});
|
||||||
258
public/app/plugins/panel/xychart/v2/migrations.ts
Normal file
258
public/app/plugins/panel/xychart/v2/migrations.ts
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
import { FieldMatcherID, FrameMatcherID, MatcherConfig, PanelModel } from '@grafana/data';
|
||||||
|
|
||||||
|
import { ScatterSeriesConfig, SeriesMapping, XYDimensionConfig, Options as PrevOptions } from '../panelcfg.gen';
|
||||||
|
|
||||||
|
import { XYSeriesConfig, Options } from './panelcfg.gen';
|
||||||
|
|
||||||
|
// export const xyChartChangeHandler: PanelTypeChangedHandler = (
|
||||||
|
// panel,
|
||||||
|
// prevPluginId,
|
||||||
|
// prevOptions,
|
||||||
|
// prevFieldConfig
|
||||||
|
// ): Options => {
|
||||||
|
// if (prevPluginId === 'xychart') {
|
||||||
|
// return migrateOptions({
|
||||||
|
// options: prevOptions,
|
||||||
|
// fieldConfig: prevFieldConfig,
|
||||||
|
// } as PanelModel);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return prevOptions as Options;
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const xyChartMigrationHandler = (panel: PanelModel): Options => {
|
||||||
|
const pluginVersion = panel?.pluginVersion ?? '';
|
||||||
|
|
||||||
|
if (pluginVersion === '') {
|
||||||
|
return migrateOptions(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return panel.options as Options;
|
||||||
|
};
|
||||||
|
|
||||||
|
function migrateOptions(panel: PanelModel): Options {
|
||||||
|
const { dims, seriesMapping, series: oldSeries, ...cleanedOpts } = panel.options as PrevOptions;
|
||||||
|
const { exclude = [], frame: frameShared, x: xShared }: XYDimensionConfig = dims ?? {};
|
||||||
|
|
||||||
|
const custDefaults = panel.fieldConfig.defaults.custom;
|
||||||
|
|
||||||
|
let oldSeries2 = oldSeries;
|
||||||
|
|
||||||
|
if (seriesMapping === SeriesMapping.Auto) {
|
||||||
|
oldSeries2 = [
|
||||||
|
{
|
||||||
|
x: undefined,
|
||||||
|
y: undefined,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// old manual mode example
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"pointColor": {
|
||||||
|
"fixed": "purple" // this becomes override for y field.config.custom.pointColor.fixed, (or config.color?)
|
||||||
|
"field": "BMI Male", // ...unless another field is mapped, then ignore
|
||||||
|
},
|
||||||
|
"pointSize": {
|
||||||
|
"field": "Weight Male",
|
||||||
|
"max": 40, // this becomes override for y field.config.custom.pointSize.max
|
||||||
|
"min": 1, // ...and .min
|
||||||
|
"fixed": 50.5
|
||||||
|
},
|
||||||
|
"frame": 0, // byIndex frame matcher
|
||||||
|
"x": "Height Male", // byName field matcher, falls back to byType/number field matcher
|
||||||
|
"y": "Weight Male" // byName field matcher, falls back to byType/number field matcher
|
||||||
|
}
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
const newSeries: XYSeriesConfig[] = oldSeries2.map(({ x, y, pointColor, pointSize, frame }: ScatterSeriesConfig) => {
|
||||||
|
const { fixed: colorFixed, field: colorField } = pointColor ?? {};
|
||||||
|
const { fixed: sizeFixed, field: sizeField, min: sizeMin, max: sizeMax } = pointSize ?? {};
|
||||||
|
|
||||||
|
let xMatcherConfig: MatcherConfig;
|
||||||
|
let yMatcherConfig: MatcherConfig;
|
||||||
|
|
||||||
|
// old auto mode did not require x field defined
|
||||||
|
if (x == null && xShared == null) {
|
||||||
|
// TODO: this should just be the internal default. no need to store on save model
|
||||||
|
xMatcherConfig = {
|
||||||
|
id: FieldMatcherID.byType,
|
||||||
|
options: 'number',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
xMatcherConfig = {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: x ?? xShared,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y == null) {
|
||||||
|
// TODO: this should just be the internal default. no need to store on save model
|
||||||
|
yMatcherConfig = {
|
||||||
|
id: FieldMatcherID.byType,
|
||||||
|
options: 'number',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
yMatcherConfig = {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorField == null && colorFixed && custDefaults.pointColor?.fixed !== colorFixed) {
|
||||||
|
// NOTE: intentionally not using custom.pointColor.fixed
|
||||||
|
let hasOverride = panel.fieldConfig.overrides.some(
|
||||||
|
(o) =>
|
||||||
|
o.matcher.id === yMatcherConfig.id &&
|
||||||
|
o.matcher.options === yMatcherConfig.options &&
|
||||||
|
o.properties.some((p) => p.id === 'color')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasOverride) {
|
||||||
|
panel.fieldConfig.overrides.push({
|
||||||
|
matcher: yMatcherConfig,
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
id: 'color',
|
||||||
|
value: {
|
||||||
|
mode: 'fixed',
|
||||||
|
fixedColor: colorFixed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add field overrides for custom pointSize.fixed
|
||||||
|
if (sizeField == null && sizeFixed && custDefaults.pointSize?.fixed !== sizeFixed) {
|
||||||
|
let hasOverride = panel.fieldConfig.overrides.some(
|
||||||
|
(o) =>
|
||||||
|
o.matcher.id === yMatcherConfig.id &&
|
||||||
|
o.matcher.options === yMatcherConfig.options &&
|
||||||
|
o.properties.some((p) => p.id === 'custom.pointSize.fixed')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasOverride) {
|
||||||
|
panel.fieldConfig.overrides.push({
|
||||||
|
matcher: yMatcherConfig,
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
id: 'custom.pointSize.fixed',
|
||||||
|
value: sizeFixed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizeField != null) {
|
||||||
|
// add field overrides for custom pointSize.min
|
||||||
|
if (sizeMin && custDefaults.pointSize?.min !== sizeMin) {
|
||||||
|
let hasOverride = panel.fieldConfig.overrides.some(
|
||||||
|
(o) =>
|
||||||
|
o.matcher.id === yMatcherConfig.id &&
|
||||||
|
o.matcher.options === yMatcherConfig.options &&
|
||||||
|
o.properties.some((p) => p.id === 'custom.pointSize.min')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasOverride) {
|
||||||
|
panel.fieldConfig.overrides.push({
|
||||||
|
matcher: {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: sizeField,
|
||||||
|
},
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
id: 'custom.pointSize.min',
|
||||||
|
value: sizeMin,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add field overrides for custom pointSize.min
|
||||||
|
if (sizeMax && custDefaults.pointSize?.max !== sizeMax) {
|
||||||
|
let hasOverride = panel.fieldConfig.overrides.some(
|
||||||
|
(o) =>
|
||||||
|
o.matcher.id === yMatcherConfig.id &&
|
||||||
|
o.matcher.options === yMatcherConfig.options &&
|
||||||
|
o.properties.some((p) => p.id === 'custom.pointSize.max')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasOverride) {
|
||||||
|
panel.fieldConfig.overrides.push({
|
||||||
|
matcher: {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: sizeField,
|
||||||
|
},
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
id: 'custom.pointSize.max',
|
||||||
|
value: sizeMax,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
frame: {
|
||||||
|
matcher: {
|
||||||
|
id: FrameMatcherID.byIndex,
|
||||||
|
options: frame ?? (seriesMapping === SeriesMapping.Manual ? i++ : frameShared ?? 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
matcher: xMatcherConfig,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
matcher: yMatcherConfig,
|
||||||
|
...(exclude.length && {
|
||||||
|
exclude: {
|
||||||
|
id: FieldMatcherID.byNames,
|
||||||
|
options: exclude,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...(colorField && {
|
||||||
|
color: {
|
||||||
|
matcher: {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: colorField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(sizeField && {
|
||||||
|
size: {
|
||||||
|
matcher: {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: sizeField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const newOptions: Options = {
|
||||||
|
...cleanedOpts,
|
||||||
|
mapping: seriesMapping === SeriesMapping.Auto ? SeriesMapping.Auto : SeriesMapping.Manual,
|
||||||
|
series: newSeries,
|
||||||
|
};
|
||||||
|
|
||||||
|
custDefaults.pointSize = custDefaults.pointSize.fixed;
|
||||||
|
|
||||||
|
// panel.fieldConfig = {
|
||||||
|
// defaults,
|
||||||
|
// overrides,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// console.log('xyChartMigrationHandler', panel.options, newOptions);
|
||||||
|
|
||||||
|
return newOptions;
|
||||||
|
}
|
||||||
37
public/app/plugins/panel/xychart/v2/module.tsx
Normal file
37
public/app/plugins/panel/xychart/v2/module.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { PanelPlugin } from '@grafana/data';
|
||||||
|
import { commonOptionsBuilder } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { SeriesEditor } from './SeriesEditor';
|
||||||
|
import { XYChartPanel2 } from './XYChartPanel';
|
||||||
|
import { getScatterFieldConfig } from './config';
|
||||||
|
import { xyChartMigrationHandler } from './migrations';
|
||||||
|
import { FieldConfig, defaultFieldConfig, Options } from './panelcfg.gen';
|
||||||
|
|
||||||
|
export const plugin = new PanelPlugin<Options, FieldConfig>(XYChartPanel2)
|
||||||
|
// .setPanelChangeHandler(xyChartChangeHandler)
|
||||||
|
.setMigrationHandler(xyChartMigrationHandler)
|
||||||
|
.useFieldConfig(getScatterFieldConfig(defaultFieldConfig))
|
||||||
|
.setPanelOptions((builder) => {
|
||||||
|
builder
|
||||||
|
.addRadio({
|
||||||
|
path: 'mapping',
|
||||||
|
name: 'Series mapping',
|
||||||
|
defaultValue: 'auto',
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ value: 'auto', label: 'Auto' },
|
||||||
|
{ value: 'manual', label: 'Manual' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'series',
|
||||||
|
path: 'series',
|
||||||
|
name: '',
|
||||||
|
editor: SeriesEditor,
|
||||||
|
defaultValue: [{}],
|
||||||
|
});
|
||||||
|
|
||||||
|
commonOptionsBuilder.addTooltipOptions(builder, true);
|
||||||
|
commonOptionsBuilder.addLegendOptions(builder);
|
||||||
|
});
|
||||||
82
public/app/plugins/panel/xychart/v2/panelcfg.cue
Normal file
82
public/app/plugins/panel/xychart/v2/panelcfg.cue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2023 Grafana Labs
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package grafanaplugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
composableKinds: PanelCfg: {
|
||||||
|
maturity: "experimental"
|
||||||
|
|
||||||
|
lineage: {
|
||||||
|
schemas: [{
|
||||||
|
version: [0, 0]
|
||||||
|
schema: {
|
||||||
|
SeriesMapping: "auto" | "manual" @cuetsy(kind="enum")
|
||||||
|
XYShowMode: "points" | "lines" | "points+lines" @cuetsy(kind="enum", memberNames="Points|Lines|PointsAndLines")
|
||||||
|
|
||||||
|
// NOTE: (copied from dashboard_kind.cue, since not exported)
|
||||||
|
// Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
|
||||||
|
// It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.
|
||||||
|
#MatcherConfig: {
|
||||||
|
// The matcher id. This is used to find the matcher implementation from registry.
|
||||||
|
id: string | *"" @grafanamaturity(NeedsExpertReview)
|
||||||
|
// The matcher options. This is specific to the matcher implementation.
|
||||||
|
options?: _ @grafanamaturity(NeedsExpertReview)
|
||||||
|
} @cuetsy(kind="interface") @grafana(TSVeneer="type")
|
||||||
|
|
||||||
|
FieldConfig: {
|
||||||
|
common.HideableFieldConfig
|
||||||
|
common.AxisConfig
|
||||||
|
|
||||||
|
show?: XYShowMode & (*"points" | _)
|
||||||
|
|
||||||
|
pointSize?: {
|
||||||
|
fixed?: int32 & >=0
|
||||||
|
min?: int32 & >=0
|
||||||
|
max?: int32 & >=0
|
||||||
|
}
|
||||||
|
|
||||||
|
// pointSymbol?: common.ResourceDimensionConfig
|
||||||
|
// fillOpacity?: number & >=0 & <=1 | *0.5
|
||||||
|
// lineColor?: common.ColorDimensionConfig
|
||||||
|
|
||||||
|
lineWidth?: int32 & >=0
|
||||||
|
lineStyle?: common.LineStyle
|
||||||
|
} @cuetsy(kind="interface",TSVeneer="type")
|
||||||
|
|
||||||
|
XYSeriesConfig: {
|
||||||
|
name?: { fixed?: string }
|
||||||
|
frame?: { matcher: #MatcherConfig }
|
||||||
|
x?: { matcher: #MatcherConfig }
|
||||||
|
y?: { matcher: #MatcherConfig }
|
||||||
|
color?: { matcher: #MatcherConfig }
|
||||||
|
size?: { matcher: #MatcherConfig }
|
||||||
|
} @cuetsy(kind="interface")
|
||||||
|
|
||||||
|
Options: {
|
||||||
|
common.OptionsWithLegend
|
||||||
|
common.OptionsWithTooltip
|
||||||
|
|
||||||
|
mapping: SeriesMapping
|
||||||
|
|
||||||
|
series: [...XYSeriesConfig]
|
||||||
|
} @cuetsy(kind="interface")
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
lenses: []
|
||||||
|
}
|
||||||
|
}
|
||||||
87
public/app/plugins/panel/xychart/v2/panelcfg.gen.ts
Normal file
87
public/app/plugins/panel/xychart/v2/panelcfg.gen.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||||
|
//
|
||||||
|
// Generated by:
|
||||||
|
// public/app/plugins/gen.go
|
||||||
|
// Using jennies:
|
||||||
|
// TSTypesJenny
|
||||||
|
// PluginTsTypesJenny
|
||||||
|
//
|
||||||
|
// Run 'make gen-cue' from repository root to regenerate.
|
||||||
|
|
||||||
|
import * as common from '@grafana/schema';
|
||||||
|
|
||||||
|
export enum SeriesMapping {
|
||||||
|
Auto = 'auto',
|
||||||
|
Manual = 'manual',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum XYShowMode {
|
||||||
|
Lines = 'lines',
|
||||||
|
Points = 'points',
|
||||||
|
PointsAndLines = 'points+lines',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: (copied from dashboard_kind.cue, since not exported)
|
||||||
|
* Matcher is a predicate configuration. Based on the config a set of field(s) or values is filtered in order to apply override / transformation.
|
||||||
|
* It comes with in id ( to resolve implementation from registry) and a configuration that’s specific to a particular matcher type.
|
||||||
|
*/
|
||||||
|
export interface MatcherConfig {
|
||||||
|
/**
|
||||||
|
* The matcher id. This is used to find the matcher implementation from registry.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The matcher options. This is specific to the matcher implementation.
|
||||||
|
*/
|
||||||
|
options?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultMatcherConfig: Partial<MatcherConfig> = {
|
||||||
|
id: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FieldConfig extends common.HideableFieldConfig, common.AxisConfig {
|
||||||
|
lineStyle?: common.LineStyle;
|
||||||
|
lineWidth?: number;
|
||||||
|
pointSize?: {
|
||||||
|
fixed?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
};
|
||||||
|
show?: XYShowMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultFieldConfig: Partial<FieldConfig> = {
|
||||||
|
show: XYShowMode.Points,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface XYSeriesConfig {
|
||||||
|
color?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
frame?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
name?: {
|
||||||
|
fixed?: string;
|
||||||
|
};
|
||||||
|
size?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
x?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
y?: {
|
||||||
|
matcher: MatcherConfig;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
|
||||||
|
mapping: SeriesMapping;
|
||||||
|
series: Array<XYSeriesConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOptions: Partial<Options> = {
|
||||||
|
series: [],
|
||||||
|
};
|
||||||
19
public/app/plugins/panel/xychart/v2/plugin.json
Normal file
19
public/app/plugins/panel/xychart/v2/plugin.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"type": "panel",
|
||||||
|
"name": "XY Chart",
|
||||||
|
"id": "xychart",
|
||||||
|
"state": "beta",
|
||||||
|
|
||||||
|
"info": {
|
||||||
|
"description": "Supports arbitrary X vs Y in a graph to visualize the relationship between two variables.",
|
||||||
|
"keywords": ["scatter", "plot"],
|
||||||
|
"author": {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "https://grafana.com"
|
||||||
|
},
|
||||||
|
"logos": {
|
||||||
|
"small": "img/icn-xychart.svg",
|
||||||
|
"large": "img/icn-xychart.svg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
482
public/app/plugins/panel/xychart/v2/scatter.ts
Normal file
482
public/app/plugins/panel/xychart/v2/scatter.ts
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
|
import { formattedValueToString, GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||||
|
import { AxisPlacement, ScaleDirection, ScaleOrientation, VisibilityMode } from '@grafana/schema';
|
||||||
|
import { UPlotConfigBuilder } from '@grafana/ui';
|
||||||
|
import { FacetedData, FacetSeries } from '@grafana/ui/src/components/uPlot/types';
|
||||||
|
|
||||||
|
import { pointWithin, Quadtree, Rect } from '../../barchart/quadtree';
|
||||||
|
|
||||||
|
import { XYSeries } from './types2';
|
||||||
|
import { getCommonPrefixSuffix } from './utils';
|
||||||
|
|
||||||
|
interface DrawBubblesOpts {
|
||||||
|
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void;
|
||||||
|
disp: {
|
||||||
|
//unit: 3,
|
||||||
|
size: {
|
||||||
|
values: (u: uPlot, seriesIdx: number) => number[];
|
||||||
|
};
|
||||||
|
color: {
|
||||||
|
values: (u: uPlot, seriesIdx: number) => string[];
|
||||||
|
alpha: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
|
||||||
|
if (xySeries.length === 0) {
|
||||||
|
return { builder: null, prepData: () => [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
let qt: Quadtree;
|
||||||
|
let hRect: Rect | null;
|
||||||
|
|
||||||
|
function drawBubblesFactory(opts: DrawBubblesOpts) {
|
||||||
|
const drawBubbles: uPlot.Series.PathBuilder = (u, seriesIdx, idx0, idx1) => {
|
||||||
|
uPlot.orient(
|
||||||
|
u,
|
||||||
|
seriesIdx,
|
||||||
|
(
|
||||||
|
series,
|
||||||
|
dataX,
|
||||||
|
dataY,
|
||||||
|
scaleX,
|
||||||
|
scaleY,
|
||||||
|
valToPosX,
|
||||||
|
valToPosY,
|
||||||
|
xOff,
|
||||||
|
yOff,
|
||||||
|
xDim,
|
||||||
|
yDim,
|
||||||
|
moveTo,
|
||||||
|
lineTo,
|
||||||
|
rect,
|
||||||
|
arc
|
||||||
|
) => {
|
||||||
|
const pxRatio = uPlot.pxRatio;
|
||||||
|
const scatterInfo = xySeries[seriesIdx - 1];
|
||||||
|
let d = u.data[seriesIdx] as unknown as FacetSeries;
|
||||||
|
|
||||||
|
// showLine: boolean;
|
||||||
|
// lineStyle: common.LineStyle;
|
||||||
|
// showPoints: common.VisibilityMode;
|
||||||
|
|
||||||
|
let showLine = scatterInfo.showLine;
|
||||||
|
let showPoints = scatterInfo.showPoints === VisibilityMode.Always;
|
||||||
|
|
||||||
|
let strokeWidth = 1;
|
||||||
|
|
||||||
|
u.ctx.save();
|
||||||
|
|
||||||
|
u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||||
|
u.ctx.clip();
|
||||||
|
|
||||||
|
u.ctx.fillStyle = (series.fill as any)(); // assumes constant
|
||||||
|
u.ctx.strokeStyle = (series.stroke as any)();
|
||||||
|
u.ctx.lineWidth = strokeWidth;
|
||||||
|
|
||||||
|
let deg360 = 2 * Math.PI;
|
||||||
|
|
||||||
|
let xKey = scaleX.key!;
|
||||||
|
let yKey = scaleY.key!;
|
||||||
|
|
||||||
|
// let pointHints = scatterInfo.hints.pointSize;
|
||||||
|
// const colorByValue = scatterInfo.hints.pointColor.mode.isByValue;
|
||||||
|
const pointHints = { max: undefined, fixed: 5 };
|
||||||
|
const colorByValue = false;
|
||||||
|
|
||||||
|
let maxSize = (pointHints.max ?? pointHints.fixed) * pxRatio;
|
||||||
|
|
||||||
|
// todo: this depends on direction & orientation
|
||||||
|
// todo: calc once per redraw, not per path
|
||||||
|
let filtLft = u.posToVal(-maxSize / 2, xKey);
|
||||||
|
let filtRgt = u.posToVal(u.bbox.width / pxRatio + maxSize / 2, xKey);
|
||||||
|
let filtBtm = u.posToVal(u.bbox.height / pxRatio + maxSize / 2, yKey);
|
||||||
|
let filtTop = u.posToVal(-maxSize / 2, yKey);
|
||||||
|
|
||||||
|
let sizes = opts.disp.size.values(u, seriesIdx);
|
||||||
|
let pointColors = opts.disp.color.values(u, seriesIdx);
|
||||||
|
let pointAlpha = opts.disp.color.alpha;
|
||||||
|
|
||||||
|
let linePath: Path2D | null = showLine ? new Path2D() : null;
|
||||||
|
|
||||||
|
let curColor: CanvasRenderingContext2D['fillStyle'] | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < d[0].length; i++) {
|
||||||
|
let xVal = d[0][i];
|
||||||
|
let yVal = d[1][i];
|
||||||
|
let size = sizes[i] * pxRatio;
|
||||||
|
|
||||||
|
if (xVal >= filtLft && xVal <= filtRgt && yVal >= filtBtm && yVal <= filtTop) {
|
||||||
|
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||||
|
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||||
|
|
||||||
|
if (showLine) {
|
||||||
|
linePath!.lineTo(cx, cy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPoints) {
|
||||||
|
// if pointHints.fixed? don't recalc size
|
||||||
|
// if pointColor has 0 opacity, draw as single path (assuming all strokes are alpha 1)
|
||||||
|
|
||||||
|
u.ctx.moveTo(cx + size / 2, cy);
|
||||||
|
u.ctx.beginPath();
|
||||||
|
u.ctx.arc(cx, cy, size / 2, 0, deg360);
|
||||||
|
|
||||||
|
if (colorByValue) {
|
||||||
|
if (pointColors[i] !== curColor) {
|
||||||
|
curColor = pointColors[i];
|
||||||
|
u.ctx.fillStyle = alpha(curColor, pointAlpha);
|
||||||
|
u.ctx.strokeStyle = curColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ctx.fill();
|
||||||
|
u.ctx.stroke();
|
||||||
|
opts.each(
|
||||||
|
u,
|
||||||
|
seriesIdx,
|
||||||
|
i,
|
||||||
|
cx - size / 2 - strokeWidth / 2,
|
||||||
|
cy - size / 2 - strokeWidth / 2,
|
||||||
|
size + strokeWidth,
|
||||||
|
size + strokeWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLine) {
|
||||||
|
u.ctx.strokeStyle = scatterInfo.color.fixed!;
|
||||||
|
u.ctx.lineWidth = scatterInfo.lineWidth * pxRatio;
|
||||||
|
|
||||||
|
const { lineStyle } = scatterInfo;
|
||||||
|
if (lineStyle && lineStyle.fill !== 'solid') {
|
||||||
|
if (lineStyle.fill === 'dot') {
|
||||||
|
u.ctx.lineCap = 'round';
|
||||||
|
}
|
||||||
|
u.ctx.setLineDash(lineStyle.dash ?? [10, 10]);
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ctx.stroke(linePath!);
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ctx.restore();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return drawBubbles;
|
||||||
|
}
|
||||||
|
|
||||||
|
let drawBubbles = drawBubblesFactory({
|
||||||
|
disp: {
|
||||||
|
size: {
|
||||||
|
//unit: 3, // raw CSS pixels
|
||||||
|
values: (u, seriesIdx) => {
|
||||||
|
return u.data[seriesIdx][2] as any; // already contains final pixel geometry
|
||||||
|
//let [minValue, maxValue] = getSizeMinMax(u);
|
||||||
|
//return u.data[seriesIdx][2].map(v => getSize(v, minValue, maxValue));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
// string values
|
||||||
|
values: (u, seriesIdx) => {
|
||||||
|
return u.data[seriesIdx][3] as any;
|
||||||
|
},
|
||||||
|
alpha: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
|
||||||
|
// we get back raw canvas coords (included axes & padding). translate to the plotting area origin
|
||||||
|
lft -= u.bbox.left;
|
||||||
|
top -= u.bbox.top;
|
||||||
|
qt.add({ x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const builder = new UPlotConfigBuilder();
|
||||||
|
|
||||||
|
builder.setCursor({
|
||||||
|
drag: { setScale: true },
|
||||||
|
dataIdx: (u, seriesIdx) => {
|
||||||
|
if (seriesIdx === 1) {
|
||||||
|
const pxRatio = uPlot.pxRatio;
|
||||||
|
|
||||||
|
hRect = null;
|
||||||
|
|
||||||
|
let dist = Infinity;
|
||||||
|
let cx = u.cursor.left! * pxRatio;
|
||||||
|
let cy = u.cursor.top! * pxRatio;
|
||||||
|
|
||||||
|
qt.get(cx, cy, 1, 1, (o) => {
|
||||||
|
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
|
||||||
|
let ocx = o.x + o.w / 2;
|
||||||
|
let ocy = o.y + o.h / 2;
|
||||||
|
|
||||||
|
let dx = ocx - cx;
|
||||||
|
let dy = ocy - cy;
|
||||||
|
|
||||||
|
let d = Math.sqrt(dx ** 2 + dy ** 2);
|
||||||
|
|
||||||
|
// test against radius for actual hover
|
||||||
|
if (d <= o.w / 2) {
|
||||||
|
// only hover bbox with closest distance
|
||||||
|
if (d <= dist) {
|
||||||
|
dist = d;
|
||||||
|
hRect = o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hRect && seriesIdx === hRect.sidx ? hRect.didx : null;
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
size: (u, seriesIdx) => {
|
||||||
|
return hRect && seriesIdx === hRect.sidx ? hRect.w / uPlot.pxRatio : 0;
|
||||||
|
},
|
||||||
|
fill: (u, seriesIdx) => 'rgba(255,255,255,0.4)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// clip hover points/bubbles to plotting area
|
||||||
|
builder.addHook('init', (u, r) => {
|
||||||
|
// TODO: re-enable once we global portal again
|
||||||
|
u.over.style.overflow = 'hidden';
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addHook('drawClear', (u) => {
|
||||||
|
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||||
|
|
||||||
|
qt.clear();
|
||||||
|
|
||||||
|
// force-clear the path cache to cause drawBars() to rebuild new quadtree
|
||||||
|
u.series.forEach((s, i) => {
|
||||||
|
if (i > 0) {
|
||||||
|
// @ts-ignore
|
||||||
|
s._paths = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.setMode(2);
|
||||||
|
|
||||||
|
let xField = xySeries[0].x.field;
|
||||||
|
|
||||||
|
let fieldConfig = xField.config;
|
||||||
|
let customConfig = fieldConfig.custom;
|
||||||
|
let scaleDistr = customConfig?.scaleDistribution;
|
||||||
|
|
||||||
|
builder.addScale({
|
||||||
|
scaleKey: 'x',
|
||||||
|
isTime: false,
|
||||||
|
orientation: ScaleOrientation.Horizontal,
|
||||||
|
direction: ScaleDirection.Right,
|
||||||
|
distribution: scaleDistr?.type,
|
||||||
|
log: scaleDistr?.log,
|
||||||
|
linearThreshold: scaleDistr?.linearThreshold,
|
||||||
|
min: fieldConfig.min,
|
||||||
|
max: fieldConfig.max,
|
||||||
|
softMin: customConfig?.axisSoftMin,
|
||||||
|
softMax: customConfig?.axisSoftMax,
|
||||||
|
centeredZero: customConfig?.axisCenteredZero,
|
||||||
|
decimals: fieldConfig.decimals,
|
||||||
|
});
|
||||||
|
|
||||||
|
// why does this fall back to '' instead of null or undef?
|
||||||
|
let xAxisLabel = customConfig.axisLabel;
|
||||||
|
|
||||||
|
if (xAxisLabel == null || xAxisLabel === '') {
|
||||||
|
let dispNames = xySeries.map((s) => s.x.field.state?.displayName ?? '');
|
||||||
|
|
||||||
|
let xAxisAutoLabel =
|
||||||
|
xySeries.length === 1
|
||||||
|
? xField.state?.displayName ?? xField.name
|
||||||
|
: new Set(dispNames).size === 1
|
||||||
|
? dispNames[0]
|
||||||
|
: getCommonPrefixSuffix(dispNames);
|
||||||
|
|
||||||
|
if (xAxisAutoLabel !== '') {
|
||||||
|
xAxisLabel = xAxisAutoLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addAxis({
|
||||||
|
scaleKey: 'x',
|
||||||
|
placement: customConfig?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden,
|
||||||
|
show: customConfig?.axisPlacement !== AxisPlacement.Hidden,
|
||||||
|
grid: { show: customConfig?.axisGridShow },
|
||||||
|
border: { show: customConfig?.axisBorderShow },
|
||||||
|
theme,
|
||||||
|
label: xAxisLabel,
|
||||||
|
formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)),
|
||||||
|
});
|
||||||
|
|
||||||
|
xySeries.forEach((s, si) => {
|
||||||
|
let field = s.y.field;
|
||||||
|
|
||||||
|
const lineColor = s.color.fixed;
|
||||||
|
const pointColor = s.color.fixed;
|
||||||
|
//const lineColor = s.lineColor(frame);
|
||||||
|
//const lineWidth = s.lineWidth;
|
||||||
|
|
||||||
|
let scaleKey = field.config.unit ?? 'y';
|
||||||
|
let config = field.config;
|
||||||
|
let customConfig = config.custom;
|
||||||
|
let scaleDistr = customConfig?.scaleDistribution;
|
||||||
|
|
||||||
|
builder.addScale({
|
||||||
|
scaleKey,
|
||||||
|
orientation: ScaleOrientation.Vertical,
|
||||||
|
direction: ScaleDirection.Up,
|
||||||
|
distribution: scaleDistr?.type,
|
||||||
|
log: scaleDistr?.log,
|
||||||
|
linearThreshold: scaleDistr?.linearThreshold,
|
||||||
|
min: config.min,
|
||||||
|
max: config.max,
|
||||||
|
softMin: customConfig?.axisSoftMin,
|
||||||
|
softMax: customConfig?.axisSoftMax,
|
||||||
|
centeredZero: customConfig?.axisCenteredZero,
|
||||||
|
decimals: config.decimals,
|
||||||
|
});
|
||||||
|
|
||||||
|
// why does this fall back to '' instead of null or undef?
|
||||||
|
let yAxisLabel = customConfig.axisLabel;
|
||||||
|
|
||||||
|
if (yAxisLabel == null || yAxisLabel === '') {
|
||||||
|
let dispNames = xySeries.map((s) => s.y.field.state?.displayName ?? '');
|
||||||
|
|
||||||
|
let yAxisAutoLabel =
|
||||||
|
xySeries.length === 1
|
||||||
|
? field.state?.displayName ?? field.name
|
||||||
|
: new Set(dispNames).size === 1
|
||||||
|
? dispNames[0]
|
||||||
|
: getCommonPrefixSuffix(dispNames);
|
||||||
|
|
||||||
|
if (yAxisAutoLabel !== '') {
|
||||||
|
yAxisLabel = yAxisAutoLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addAxis({
|
||||||
|
scaleKey,
|
||||||
|
theme,
|
||||||
|
placement: customConfig?.axisPlacement === AxisPlacement.Auto ? AxisPlacement.Left : customConfig?.axisPlacement,
|
||||||
|
show: customConfig?.axisPlacement !== AxisPlacement.Hidden,
|
||||||
|
grid: { show: customConfig?.axisGridShow },
|
||||||
|
border: { show: customConfig?.axisBorderShow },
|
||||||
|
size: customConfig?.axisWidth,
|
||||||
|
// label: yAxisLabel == null || yAxisLabel === '' ? fieldDisplayName : yAxisLabel,
|
||||||
|
label: yAxisLabel,
|
||||||
|
formatValue: (v, decimals) => formattedValueToString(field.display!(v, decimals)),
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.addSeries({
|
||||||
|
facets: [
|
||||||
|
{
|
||||||
|
scale: 'x',
|
||||||
|
auto: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: scaleKey,
|
||||||
|
auto: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pathBuilder: drawBubbles, // drawBubbles({disp: {size: {values: () => }}})
|
||||||
|
theme,
|
||||||
|
scaleKey: '', // facets' scales used (above)
|
||||||
|
lineColor: alpha('' + lineColor, 1),
|
||||||
|
fillColor: alpha(pointColor ?? '#ffff', 0.5),
|
||||||
|
show: !field.state?.hideFrom?.viz,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
builder.setPrepData((frames) => {
|
||||||
|
let seriesData = lookup.fieldMaps.flatMap((f, i) => {
|
||||||
|
let { fields } = frames[i];
|
||||||
|
|
||||||
|
return f.y.map((yIndex, frameSeriesIndex) => {
|
||||||
|
let xValues = fields[f.x[frameSeriesIndex]].values;
|
||||||
|
let yValues = fields[f.y[frameSeriesIndex]].values;
|
||||||
|
let sizeValues = f.size;
|
||||||
|
|
||||||
|
if (!Array.isArray(sizeValues)) {
|
||||||
|
sizeValues = Array(xValues.length).fill(sizeValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [xValues, yValues, sizeValues];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return [null, ...seriesData];
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
return { builder, prepData };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrepData = (xySeries: XYSeries[]) => FacetedData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called everytime the data changes
|
||||||
|
*
|
||||||
|
* from? is this where we would support that? -- need the previous values
|
||||||
|
*/
|
||||||
|
export function prepData(xySeries: XYSeries[]): FacetedData {
|
||||||
|
// if (info.error || !data.length) {
|
||||||
|
// return [null];
|
||||||
|
// }
|
||||||
|
|
||||||
|
return [
|
||||||
|
null,
|
||||||
|
...xySeries.map((s, idx) => {
|
||||||
|
let len = s.x.field.values.length;
|
||||||
|
|
||||||
|
let diams: number[];
|
||||||
|
|
||||||
|
if (s.size.field != null) {
|
||||||
|
let { min, max } = s.size;
|
||||||
|
|
||||||
|
// todo: this scaling should be in renderer from raw values (not by passing css pixel diams via data)
|
||||||
|
let minPx = min! ** 2;
|
||||||
|
let maxPx = max! ** 2;
|
||||||
|
// use quadratic size scaling in byValue modes
|
||||||
|
let pxRange = maxPx - minPx;
|
||||||
|
|
||||||
|
// todo: add shared, local, or key-group min/max option?
|
||||||
|
// todo: better min/max with ignoring non-finite values
|
||||||
|
// todo: allow this to come from fieldConfig min/max ? or field.state.min/max (shared)
|
||||||
|
let vals = s.size.field.values;
|
||||||
|
let minVal = Math.min(...vals);
|
||||||
|
let maxVal = Math.max(...vals);
|
||||||
|
let valRange = maxVal - minVal;
|
||||||
|
|
||||||
|
diams = Array(len);
|
||||||
|
|
||||||
|
for (let i = 0; i < vals.length; i++) {
|
||||||
|
let val = vals[i];
|
||||||
|
|
||||||
|
let valPct = (val - minVal) / valRange;
|
||||||
|
let pxArea = minPx + valPct * pxRange;
|
||||||
|
diams[i] = pxArea ** 0.5;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
diams = Array(len).fill(s.size.fixed!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
s.x.field.values, // X
|
||||||
|
s.y.field.values, // Y
|
||||||
|
diams, // TODO: fails for by value
|
||||||
|
Array(len).fill(s.color.fixed!), // TODO: fails for by value
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
88
public/app/plugins/panel/xychart/v2/types2.ts
Normal file
88
public/app/plugins/panel/xychart/v2/types2.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Field } from '@grafana/data';
|
||||||
|
import * as common from '@grafana/schema';
|
||||||
|
|
||||||
|
// import { SeriesMapping, XYSeriesConfig } from './panelcfg.gen';
|
||||||
|
|
||||||
|
// // panel save model
|
||||||
|
// export interface XYSeriesConfig {
|
||||||
|
// name?: {
|
||||||
|
// fixed?: string; // (if explicitly defined in manual mode)
|
||||||
|
|
||||||
|
// /*
|
||||||
|
// replace?: {
|
||||||
|
// // default: 'field'
|
||||||
|
// source: 'field' | 'frame';
|
||||||
|
// // default: whatever is matched for y field
|
||||||
|
// matcher: common.MatcherConfig;
|
||||||
|
// // default: 'displayName'
|
||||||
|
// prop: 'displayName' | 'name' | 'query' | 'target';
|
||||||
|
|
||||||
|
// // similar to renameByRegex & RenameByRegexTransformerOptions
|
||||||
|
// // default: '(.*)'
|
||||||
|
// regex: string;
|
||||||
|
// // default: '$1'
|
||||||
|
// rename: string;
|
||||||
|
// }
|
||||||
|
// */
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // required in manual mode (can match same frame multiple times)
|
||||||
|
// frame?: {
|
||||||
|
// matcher: common.MatcherConfig;
|
||||||
|
// };
|
||||||
|
// x?: {
|
||||||
|
// matcher: common.MatcherConfig;
|
||||||
|
// };
|
||||||
|
// y?: {
|
||||||
|
// matcher: common.MatcherConfig;
|
||||||
|
// };
|
||||||
|
// color?: {
|
||||||
|
// matcher: common.MatcherConfig;
|
||||||
|
// };
|
||||||
|
// size?: {
|
||||||
|
// matcher: common.MatcherConfig;
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// materialized series (internal)
|
||||||
|
export interface XYSeries {
|
||||||
|
showPoints: common.VisibilityMode;
|
||||||
|
|
||||||
|
showLine: boolean;
|
||||||
|
lineWidth: number;
|
||||||
|
lineStyle: common.LineStyle;
|
||||||
|
|
||||||
|
name: {
|
||||||
|
// extracted from fieldConfig + overrides of y field
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
x: {
|
||||||
|
field: Field;
|
||||||
|
};
|
||||||
|
y: {
|
||||||
|
field: Field;
|
||||||
|
};
|
||||||
|
color: {
|
||||||
|
field?: Field;
|
||||||
|
|
||||||
|
// fixed value extracted from fieldConfig + overrides of y field
|
||||||
|
fixed?: string;
|
||||||
|
};
|
||||||
|
size: {
|
||||||
|
field?: Field;
|
||||||
|
// extracted from fieldConfig + overrides of size field
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
|
||||||
|
// fixed value extracted from fieldConfig + overrides of y field
|
||||||
|
fixed?: number;
|
||||||
|
};
|
||||||
|
// remaining unmapped fields in this frame (for showing remaining fields in tooltip)
|
||||||
|
_rest: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip {
|
||||||
|
// // source: 'annotations' | 'series', // maybe render directly from annotations (exemplars)
|
||||||
|
// mapping: SeriesMapping;
|
||||||
|
// series: XYSeriesConfig[]; // uses series[0] in auto mode to generate
|
||||||
|
// }
|
||||||
319
public/app/plugins/panel/xychart/v2/utils.ts
Normal file
319
public/app/plugins/panel/xychart/v2/utils.ts
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import {
|
||||||
|
Field,
|
||||||
|
formattedValueToString,
|
||||||
|
getFieldMatcher,
|
||||||
|
FieldType,
|
||||||
|
getFieldDisplayName,
|
||||||
|
DataFrame,
|
||||||
|
FrameMatcherID,
|
||||||
|
MatcherConfig,
|
||||||
|
FieldColorModeId,
|
||||||
|
cacheFieldDisplayNames,
|
||||||
|
FieldMatcherID,
|
||||||
|
FieldConfigSource,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { decoupleHideFromState } from '@grafana/data/src/field/fieldState';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { VisibilityMode } from '@grafana/schema';
|
||||||
|
|
||||||
|
import { XYShowMode, SeriesMapping, XYSeriesConfig } from './panelcfg.gen';
|
||||||
|
import { XYSeries } from './types2';
|
||||||
|
|
||||||
|
export function fmt(field: Field, val: number): string {
|
||||||
|
if (field.display) {
|
||||||
|
return formattedValueToString(field.display(val));
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${val}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cause we dont have a proper matcher for this currently
|
||||||
|
function getFrameMatcher2(config: MatcherConfig) {
|
||||||
|
if (config.id === FrameMatcherID.byIndex) {
|
||||||
|
return (frame: DataFrame, index: number) => index === config.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepSeries(
|
||||||
|
mapping: SeriesMapping,
|
||||||
|
mappedSeries: XYSeriesConfig[],
|
||||||
|
frames: DataFrame[],
|
||||||
|
fieldConfig: FieldConfigSource<any>
|
||||||
|
) {
|
||||||
|
cacheFieldDisplayNames(frames);
|
||||||
|
decoupleHideFromState(frames, fieldConfig);
|
||||||
|
|
||||||
|
let series: XYSeries[] = [];
|
||||||
|
|
||||||
|
if (mappedSeries.length === 0) {
|
||||||
|
mappedSeries = [{}];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { palette, getColorByName } = config.theme2.visualization;
|
||||||
|
|
||||||
|
mappedSeries.forEach((seriesCfg, seriesIdx) => {
|
||||||
|
if (mapping === SeriesMapping.Manual) {
|
||||||
|
if (seriesCfg.frame?.matcher == null || seriesCfg.x?.matcher == null || seriesCfg.y?.matcher == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let xMatcher = getFieldMatcher(
|
||||||
|
seriesCfg.x?.matcher ?? {
|
||||||
|
id: FieldMatcherID.byType,
|
||||||
|
options: 'number',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let yMatcher = getFieldMatcher(
|
||||||
|
seriesCfg.y?.matcher ?? {
|
||||||
|
id: FieldMatcherID.byType,
|
||||||
|
options: 'number',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let colorMatcher = seriesCfg.color ? getFieldMatcher(seriesCfg.color.matcher) : null;
|
||||||
|
let sizeMatcher = seriesCfg.size ? getFieldMatcher(seriesCfg.size.matcher) : null;
|
||||||
|
// let frameMatcher = seriesCfg.frame ? getFrameMatchers(seriesCfg.frame) : null;
|
||||||
|
let frameMatcher = seriesCfg.frame ? getFrameMatcher2(seriesCfg.frame.matcher) : null;
|
||||||
|
|
||||||
|
// loop over all frames and fields, adding a new series for each y dim
|
||||||
|
frames.forEach((frame, frameIdx) => {
|
||||||
|
// must match frame in manual mode
|
||||||
|
if (frameMatcher != null && !frameMatcher(frame, frameIdx)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// shared across each series in this frame
|
||||||
|
let restFields: Field[] = [];
|
||||||
|
|
||||||
|
let frameSeries: XYSeries[] = [];
|
||||||
|
|
||||||
|
// only grabbing number fields (exclude time, string, enum, other)
|
||||||
|
let onlyNumFields = frame.fields.filter((field) => field.type === FieldType.number);
|
||||||
|
|
||||||
|
// only one of these per frame
|
||||||
|
let x = onlyNumFields.find((field) => xMatcher(field, frame, frames));
|
||||||
|
let color =
|
||||||
|
colorMatcher != null
|
||||||
|
? onlyNumFields.find((field) => field !== x && colorMatcher!(field, frame, frames))
|
||||||
|
: undefined;
|
||||||
|
let size =
|
||||||
|
sizeMatcher != null
|
||||||
|
? onlyNumFields.find((field) => field !== x && field !== color && sizeMatcher!(field, frame, frames))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// x field is required
|
||||||
|
if (x != null) {
|
||||||
|
// match y fields and create series
|
||||||
|
onlyNumFields.forEach((field) => {
|
||||||
|
// don't reuse already-mapped fields
|
||||||
|
if (field === x || field === color || field === size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in manual mode only add single series for this config
|
||||||
|
if (mapping === SeriesMapping.Manual && frameSeries.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we match non-excluded y, create series
|
||||||
|
if (yMatcher(field, frame, frames) && !field.config.custom?.hideFrom?.viz) {
|
||||||
|
let y = field;
|
||||||
|
let name = seriesCfg.name?.fixed ?? getFieldDisplayName(y, frame, frames);
|
||||||
|
|
||||||
|
let ser: XYSeries = {
|
||||||
|
// these typically come from y field
|
||||||
|
name: {
|
||||||
|
value: name,
|
||||||
|
},
|
||||||
|
|
||||||
|
showPoints: y.config.custom.show === XYShowMode.Lines ? VisibilityMode.Never : VisibilityMode.Always,
|
||||||
|
|
||||||
|
showLine: y.config.custom.show !== XYShowMode.Points,
|
||||||
|
lineWidth: y.config.custom.lineWidth ?? 2,
|
||||||
|
lineStyle: y.config.custom.lineStyle,
|
||||||
|
// lineColor: () => seriesColor,
|
||||||
|
x: {
|
||||||
|
field: x!,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
field: y,
|
||||||
|
},
|
||||||
|
color: {},
|
||||||
|
size: {},
|
||||||
|
_rest: restFields,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (color != null) {
|
||||||
|
ser.color.field = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size != null) {
|
||||||
|
ser.size.field = size;
|
||||||
|
ser.size.min = size.config.custom.pointSize?.min ?? 5;
|
||||||
|
ser.size.max = size.config.custom.pointSize?.max ?? 100;
|
||||||
|
// ser.size.mode =
|
||||||
|
}
|
||||||
|
|
||||||
|
frameSeries.push(ser);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (frameSeries.length === 0) {
|
||||||
|
// TODO: could not create series, skip & show error?
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate rest fields
|
||||||
|
frame.fields.forEach((field) => {
|
||||||
|
let isUsedField = frameSeries.some(
|
||||||
|
({ x, y, color, size }) =>
|
||||||
|
x.field === field || y.field === field || color.field === field || size.field === field
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUsedField) {
|
||||||
|
restFields.push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
series.push(...frameSeries);
|
||||||
|
} else {
|
||||||
|
// x is missing in this frame!
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (series.length === 0) {
|
||||||
|
// TODO: could not create series, skip & show error?
|
||||||
|
} else {
|
||||||
|
// assign classic palette colors by index, as fallbacks for all series
|
||||||
|
|
||||||
|
let paletteIdx = 0;
|
||||||
|
|
||||||
|
// todo: populate min, max, mode from field + hints
|
||||||
|
series.forEach((s, i) => {
|
||||||
|
if (s.color.field == null) {
|
||||||
|
// derive fixed color from y field config
|
||||||
|
let colorCfg = s.y.field.config.color ?? { mode: FieldColorModeId.PaletteClassic };
|
||||||
|
|
||||||
|
let value = '';
|
||||||
|
|
||||||
|
if (colorCfg.mode === FieldColorModeId.PaletteClassic) {
|
||||||
|
value = getColorByName(palette[paletteIdx++ % palette.length]); // todo: do this via state.seriesIdx and re-init displayProcessor
|
||||||
|
} else if (colorCfg.mode === FieldColorModeId.Fixed) {
|
||||||
|
value = getColorByName(colorCfg.fixedColor!);
|
||||||
|
}
|
||||||
|
|
||||||
|
s.color.fixed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.size.field == null) {
|
||||||
|
// derive fixed size from y field config
|
||||||
|
s.size.fixed = s.y.field.config.custom.pointSize?.fixed ?? 5;
|
||||||
|
// ser.size.mode =
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoNameSeries(series);
|
||||||
|
|
||||||
|
// TODO: re-assign y display names?
|
||||||
|
// y.state = {
|
||||||
|
// ...y.state,
|
||||||
|
// seriesIndex: series.length + ,
|
||||||
|
// };
|
||||||
|
// y.display = getDisplayProcessor({ field, theme });
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip common prefixes and suffixes from y field names
|
||||||
|
function autoNameSeries(series: XYSeries[]) {
|
||||||
|
let names = series.map((s) => s.name.value.split(/\s+/g));
|
||||||
|
|
||||||
|
const { prefix, suffix } = findCommonPrefixSuffixLengths(names);
|
||||||
|
|
||||||
|
if (prefix < Infinity || suffix < Infinity) {
|
||||||
|
series.forEach((s, i) => {
|
||||||
|
s.name.value = names[i].slice(prefix, names[i].length - suffix).join(' ');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommonPrefixSuffix(strs: string[]) {
|
||||||
|
let names = strs.map((s) => s.split(/\s+/g));
|
||||||
|
|
||||||
|
let { prefix, suffix } = findCommonPrefixSuffixLengths(names);
|
||||||
|
|
||||||
|
let n = names[0];
|
||||||
|
|
||||||
|
if (n.length === 1 && prefix === 1 && suffix === 1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = [];
|
||||||
|
|
||||||
|
if (prefix > 0) {
|
||||||
|
parts.push(...n.slice(0, prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suffix > 0) {
|
||||||
|
parts.push(...n.slice(-suffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// lengths are in number of tokens (segments) in a phrase
|
||||||
|
function findCommonPrefixSuffixLengths(names: string[][]) {
|
||||||
|
let commonPrefixLen = Infinity;
|
||||||
|
let commonSuffixLen = Infinity;
|
||||||
|
|
||||||
|
// if auto naming strategy, rename fields by stripping common prefixes and suffixes
|
||||||
|
let segs0: string[] = names[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < names.length; i++) {
|
||||||
|
if (names[i].length < segs0.length) {
|
||||||
|
segs0 = names[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < names.length; i++) {
|
||||||
|
let segs = names[i];
|
||||||
|
|
||||||
|
if (segs !== segs0) {
|
||||||
|
// prefixes
|
||||||
|
let preLen = 0;
|
||||||
|
for (let j = 0; j < segs0.length; j++) {
|
||||||
|
if (segs[j] === segs0[j]) {
|
||||||
|
preLen++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preLen < commonPrefixLen) {
|
||||||
|
commonPrefixLen = preLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// suffixes
|
||||||
|
let sufLen = 0;
|
||||||
|
for (let j = segs0.length - 1; j >= 0; j--) {
|
||||||
|
if (segs[j] === segs0[j]) {
|
||||||
|
sufLen++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sufLen < commonSuffixLen) {
|
||||||
|
commonSuffixLen = sufLen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
prefix: commonPrefixLen,
|
||||||
|
suffix: commonSuffixLen,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user