mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
XYChart2: Implement color-by-field (#88467)
This commit is contained in:
parent
e419c76842
commit
87cafbf9af
@ -7393,9 +7393,17 @@ exports[`better eslint`] = {
|
||||
[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.", "7"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "8"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "9"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "11"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "13"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "14"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "15"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "16"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "17"]
|
||||
],
|
||||
"public/app/plugins/panel/xychart/v2/utils.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
|
1340
devenv/dev-dashboards/panel-xychart/xychart-demo.json
Normal file
1340
devenv/dev-dashboards/panel-xychart/xychart-demo.json
Normal file
File diff suppressed because one or more lines are too long
@ -946,7 +946,7 @@
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 12,
|
||||
"w": 12,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 32
|
||||
},
|
||||
@ -1830,6 +1830,315 @@
|
||||
],
|
||||
"title": "Multi-series Temperature vs Humidity",
|
||||
"type": "xychart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"fixedColor": "red",
|
||||
"mode": "continuous-BlYlRd"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": {
|
||||
"fixed": 5
|
||||
},
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"show": "points"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 12,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 32
|
||||
},
|
||||
"id": 12,
|
||||
"options": {
|
||||
"dims": {
|
||||
"frame": 0
|
||||
},
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"series": [
|
||||
{
|
||||
"name": "Price",
|
||||
"pointColor": {
|
||||
"field": "Price",
|
||||
"fixed": "#fade2a40"
|
||||
},
|
||||
"pointSize": {
|
||||
"fixed": 10,
|
||||
"max": 50,
|
||||
"min": 1
|
||||
},
|
||||
"x": "Lat",
|
||||
"y": "Lng"
|
||||
}
|
||||
],
|
||||
"seriesMapping": "manual",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_file"
|
||||
}
|
||||
],
|
||||
"title": "Color by field (gradient)",
|
||||
"type": "xychart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"fixedColor": "red",
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": {
|
||||
"fixed": 5
|
||||
},
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"show": "points"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 500
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 12,
|
||||
"w": 6,
|
||||
"x": 12,
|
||||
"y": 32
|
||||
},
|
||||
"id": 13,
|
||||
"options": {
|
||||
"dims": {
|
||||
"frame": 0
|
||||
},
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"series": [
|
||||
{
|
||||
"name": "Price",
|
||||
"pointColor": {
|
||||
"field": "Price",
|
||||
"fixed": "#fade2a40"
|
||||
},
|
||||
"pointSize": {
|
||||
"fixed": 10,
|
||||
"max": 50,
|
||||
"min": 1
|
||||
},
|
||||
"x": "Lat",
|
||||
"y": "Lng"
|
||||
}
|
||||
],
|
||||
"seriesMapping": "manual",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_file"
|
||||
}
|
||||
],
|
||||
"title": "Color by field (threshold)",
|
||||
"type": "xychart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"fixedColor": "red",
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": {
|
||||
"fixed": 5
|
||||
},
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"show": "points"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"700": {
|
||||
"color": "purple",
|
||||
"index": 0
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 12,
|
||||
"w": 6,
|
||||
"x": 18,
|
||||
"y": 32
|
||||
},
|
||||
"id": 14,
|
||||
"options": {
|
||||
"dims": {
|
||||
"frame": 0
|
||||
},
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"series": [
|
||||
{
|
||||
"name": "Price",
|
||||
"pointColor": {
|
||||
"field": "Price",
|
||||
"fixed": "#fade2a40"
|
||||
},
|
||||
"pointSize": {
|
||||
"fixed": 10,
|
||||
"max": 50,
|
||||
"min": 1
|
||||
},
|
||||
"x": "Lat",
|
||||
"y": "Lng"
|
||||
}
|
||||
],
|
||||
"seriesMapping": "manual",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"csvFileName": "flight_info_by_state.csv",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_file"
|
||||
}
|
||||
],
|
||||
"title": "Color by field (value mappings)",
|
||||
"type": "xychart"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
@ -1837,7 +2146,7 @@
|
||||
"tags": [
|
||||
"gdev",
|
||||
"panel-tests",
|
||||
"xychat"
|
||||
"graph-ng"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
@ -1848,7 +2157,7 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Panel Tests - XY Chart",
|
||||
"title": "Panel Tests - XY Chart migrations",
|
||||
"uid": "YObbMr44k",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
@ -130,7 +130,8 @@
|
||||
"timeseries-y-ticks-zero-decimals": (import '../dev-dashboards/panel-timeseries/timeseries-y-ticks-zero-decimals.json'),
|
||||
"timeseries-yaxis-ticks": (import '../dev-dashboards/panel-timeseries/timeseries-yaxis-ticks.json'),
|
||||
"trend_example": (import '../dev-dashboards/panel-trend/trend_example.json'),
|
||||
"xychart-example": (import '../dev-dashboards/panel-xychart/xychart-example.json'),
|
||||
"xychart-demo": (import '../dev-dashboards/panel-xychart/xychart-demo.json'),
|
||||
"xychart-migrations": (import '../dev-dashboards/panel-xychart/xychart-migrations.json'),
|
||||
"xychart-tooltip-color-test": (import '../dev-dashboards/panel-xychart/xychart-tooltip-color-test.json'),
|
||||
},
|
||||
}
|
||||
|
@ -36,13 +36,6 @@ export function getScatterFieldConfig(cfg: FieldConfig): SetFieldConfigOptionsAr
|
||||
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]: {
|
||||
|
@ -384,7 +384,6 @@ const prepConfig = (getData: () => DataFrame[], scatterSeries: ScatterSeries[],
|
||||
// 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);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { FALLBACK_COLOR, PanelProps } from '@grafana/data';
|
||||
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
@ -71,7 +71,7 @@ export const XYChartPanel2 = (props: Props2) => {
|
||||
items.push({
|
||||
yAxis: 1, // TODO: pull from y field
|
||||
label: s.name.value,
|
||||
color: alpha(s.color.fixed!, 1),
|
||||
color: alpha(s.color.fixed ?? FALLBACK_COLOR, 1),
|
||||
getItemKey: () => `${idx}-${s.name.value}`,
|
||||
fieldName: yField.state?.displayName ?? yField.name,
|
||||
disabled: yField.state?.hideFrom?.viz ?? false,
|
||||
|
@ -56,7 +56,7 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, i
|
||||
const headerItem: VizTooltipItem = {
|
||||
label,
|
||||
value: '',
|
||||
color: alpha(seriesColor!, 0.5),
|
||||
color: alpha(seriesColor ?? '#fff', 0.5),
|
||||
colorIndicator: ColorIndicator.marker_md,
|
||||
};
|
||||
|
||||
@ -72,14 +72,14 @@ export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, xySeries, dismiss, i
|
||||
];
|
||||
|
||||
// mapped fields for size/color
|
||||
if (sizeField != null) {
|
||||
if (sizeField != null && sizeField !== yField) {
|
||||
contentItems.push({
|
||||
label: stripSeriesName(sizeField.state?.displayName ?? sizeField.name, label),
|
||||
value: fmt(sizeField, sizeField.values[rowIndex]),
|
||||
});
|
||||
}
|
||||
|
||||
if (colorField != null) {
|
||||
if (colorField != null && colorField !== yField) {
|
||||
contentItems.push({
|
||||
label: stripSeriesName(colorField.state?.displayName ?? colorField.name, label),
|
||||
value: fmt(colorField, colorField.values[rowIndex]),
|
||||
|
@ -10,7 +10,7 @@ import { commonOptionsBuilder } from '@grafana/ui';
|
||||
|
||||
import { LineStyleEditor } from '../../timeseries/LineStyleEditor';
|
||||
|
||||
import { FieldConfig, XYShowMode } from './panelcfg.gen';
|
||||
import { FieldConfig, XYShowMode, PointShape } from './panelcfg.gen';
|
||||
|
||||
export const DEFAULT_POINT_SIZE = 5;
|
||||
|
||||
@ -36,13 +36,6 @@ export function getScatterFieldConfig(cfg: FieldConfig): SetFieldConfigOptionsAr
|
||||
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]: {
|
||||
@ -111,17 +104,39 @@ export function getScatterFieldConfig(cfg: FieldConfig): SetFieldConfigOptionsAr
|
||||
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,
|
||||
// })
|
||||
.addRadio({
|
||||
path: 'pointShape',
|
||||
name: 'Point shape',
|
||||
defaultValue: PointShape.Circle,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: PointShape.Circle, label: 'Circle' },
|
||||
{ value: PointShape.Square, label: 'Square' },
|
||||
],
|
||||
},
|
||||
showIf: (c) => c.show !== XYShowMode.Lines,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'pointStrokeWidth',
|
||||
name: 'Point stroke width',
|
||||
defaultValue: 1,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
},
|
||||
showIf: (c) => c.show !== XYShowMode.Lines,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'fillOpacity',
|
||||
name: 'Fill opacity',
|
||||
defaultValue: 50,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
},
|
||||
showIf: (c) => c.show !== XYShowMode.Lines,
|
||||
})
|
||||
.addCustomEditor<void, LineStyle>({
|
||||
id: 'lineStyle',
|
||||
path: 'lineStyle',
|
||||
|
@ -7,7 +7,7 @@ import { XYSeriesConfig, Options } from './panelcfg.gen';
|
||||
export const xyChartMigrationHandler = (panel: PanelModel): Options => {
|
||||
const pluginVersion = panel?.pluginVersion ?? '';
|
||||
|
||||
if (pluginVersion === '') {
|
||||
if (pluginVersion === '' || parseFloat(pluginVersion) < 11.1) {
|
||||
return migrateOptions(panel);
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,7 @@ composableKinds: PanelCfg: {
|
||||
schemas: [{
|
||||
version: [0, 0]
|
||||
schema: {
|
||||
PointShape: "circle" | "square" @cuetsy(kind="enum")
|
||||
SeriesMapping: "auto" | "manual" @cuetsy(kind="enum")
|
||||
XYShowMode: "points" | "lines" | "points+lines" @cuetsy(kind="enum", memberNames="Points|Lines|PointsAndLines")
|
||||
|
||||
@ -50,9 +51,11 @@ composableKinds: PanelCfg: {
|
||||
max?: int32 & >=0
|
||||
}
|
||||
|
||||
// pointSymbol?: common.ResourceDimensionConfig
|
||||
// fillOpacity?: number & >=0 & <=1 | *0.5
|
||||
// lineColor?: common.ColorDimensionConfig
|
||||
pointShape?: PointShape
|
||||
|
||||
pointStrokeWidth?: int32 & >=0
|
||||
|
||||
fillOpacity?: uint32 & <=100 | *50
|
||||
|
||||
lineWidth?: int32 & >=0
|
||||
lineStyle?: common.LineStyle
|
||||
|
@ -10,6 +10,11 @@
|
||||
|
||||
import * as common from '@grafana/schema';
|
||||
|
||||
export enum PointShape {
|
||||
Circle = 'circle',
|
||||
Square = 'square',
|
||||
}
|
||||
|
||||
export enum SeriesMapping {
|
||||
Auto = 'auto',
|
||||
Manual = 'manual',
|
||||
@ -42,17 +47,21 @@ export const defaultMatcherConfig: Partial<MatcherConfig> = {
|
||||
};
|
||||
|
||||
export interface FieldConfig extends common.HideableFieldConfig, common.AxisConfig {
|
||||
fillOpacity?: number;
|
||||
lineStyle?: common.LineStyle;
|
||||
lineWidth?: number;
|
||||
pointShape?: PointShape;
|
||||
pointSize?: {
|
||||
fixed?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
};
|
||||
pointStrokeWidth?: number;
|
||||
show?: XYShowMode;
|
||||
}
|
||||
|
||||
export const defaultFieldConfig: Partial<FieldConfig> = {
|
||||
fillOpacity: 50,
|
||||
show: XYShowMode.Points,
|
||||
};
|
||||
|
||||
|
@ -1,13 +1,26 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { formattedValueToString, GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
FALLBACK_COLOR,
|
||||
Field,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
getFieldColorModeForField,
|
||||
GrafanaTheme2,
|
||||
MappingType,
|
||||
SpecialValueMatch,
|
||||
ThresholdsMode,
|
||||
} from '@grafana/data';
|
||||
import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||
import { AxisPlacement, ScaleDirection, ScaleOrientation, VisibilityMode } from '@grafana/schema';
|
||||
import { AxisPlacement, FieldColorModeId, 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 { valuesToFills } from '../../heatmap/utils';
|
||||
|
||||
import { PointShape } from './panelcfg.gen';
|
||||
import { XYSeries } from './types2';
|
||||
import { getCommonPrefixSuffix } from './utils';
|
||||
|
||||
@ -20,7 +33,6 @@ interface DrawBubblesOpts {
|
||||
};
|
||||
color: {
|
||||
values: (u: uPlot, seriesIdx: number) => string[];
|
||||
alpha: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -65,16 +77,17 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
|
||||
|
||||
let showLine = scatterInfo.showLine;
|
||||
let showPoints = scatterInfo.showPoints === VisibilityMode.Always;
|
||||
|
||||
let strokeWidth = 1;
|
||||
let strokeWidth = scatterInfo.pointStrokeWidth ?? 0;
|
||||
|
||||
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)();
|
||||
let pointAlpha = scatterInfo.fillOpacity / 100;
|
||||
|
||||
u.ctx.fillStyle = alpha((series.fill as any)(), pointAlpha);
|
||||
u.ctx.strokeStyle = alpha((series.stroke as any)(), 1);
|
||||
u.ctx.lineWidth = strokeWidth;
|
||||
|
||||
let deg360 = 2 * Math.PI;
|
||||
@ -82,12 +95,11 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
|
||||
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;
|
||||
//const colorMode = getFieldColorModeForField(field); // isByValue
|
||||
const pointSize = scatterInfo.y.field.config.custom.pointSize;
|
||||
const colorByValue = scatterInfo.color.field != null; // && colorMode.isByValue;
|
||||
|
||||
let maxSize = (pointHints.max ?? pointHints.fixed) * pxRatio;
|
||||
let maxSize = (pointSize.max ?? pointSize.fixed) * pxRatio;
|
||||
|
||||
// todo: this depends on direction & orientation
|
||||
// todo: calc once per redraw, not per path
|
||||
@ -97,19 +109,23 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
|
||||
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 pointColors = opts.disp.color.values(u, seriesIdx);
|
||||
let pointColors = dispColors[seriesIdx - 1].values; // idxs
|
||||
let pointPalette = dispColors[seriesIdx - 1].index as Array<CanvasRenderingContext2D['fillStyle']>;
|
||||
let paletteHasAlpha = dispColors[seriesIdx - 1].hasAlpha;
|
||||
|
||||
let isSquare = scatterInfo.pointShape === PointShape.Square;
|
||||
|
||||
let linePath: Path2D | null = showLine ? new Path2D() : null;
|
||||
|
||||
let curColor: CanvasRenderingContext2D['fillStyle'] | null = null;
|
||||
let curColorIdx = -1;
|
||||
|
||||
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 size = Math.round(sizes[i] * pxRatio);
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
|
||||
@ -118,23 +134,39 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
|
||||
}
|
||||
|
||||
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;
|
||||
if (pointColors[i] !== curColorIdx) {
|
||||
curColorIdx = pointColors[i];
|
||||
let c = curColorIdx === -1 ? FALLBACK_COLOR : pointPalette[curColorIdx];
|
||||
u.ctx.fillStyle = paletteHasAlpha ? c : alpha(c as string, pointAlpha);
|
||||
u.ctx.strokeStyle = alpha(c as string, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSquare) {
|
||||
let x = Math.round(cx - size / 2);
|
||||
let y = Math.round(cy - size / 2);
|
||||
|
||||
if (colorByValue || pointAlpha > 0) {
|
||||
u.ctx.fillRect(x, y, size, size);
|
||||
}
|
||||
|
||||
if (strokeWidth > 0) {
|
||||
u.ctx.strokeRect(x, y, size, size);
|
||||
}
|
||||
} else {
|
||||
u.ctx.beginPath();
|
||||
u.ctx.arc(cx, cy, size / 2, 0, deg360);
|
||||
|
||||
if (colorByValue || pointAlpha > 0) {
|
||||
u.ctx.fill();
|
||||
}
|
||||
|
||||
if (strokeWidth > 0) {
|
||||
u.ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
u.ctx.fill();
|
||||
u.ctx.stroke();
|
||||
opts.each(
|
||||
u,
|
||||
seriesIdx,
|
||||
@ -188,7 +220,6 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
|
||||
values: (u, seriesIdx) => {
|
||||
return u.data[seriesIdx][3] as any;
|
||||
},
|
||||
alpha: 0.5,
|
||||
},
|
||||
},
|
||||
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
|
||||
@ -247,7 +278,6 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
|
||||
|
||||
// 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';
|
||||
});
|
||||
|
||||
@ -391,95 +421,92 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
|
||||
pathBuilder: drawBubbles, // drawBubbles({disp: {size: {values: () => }}})
|
||||
theme,
|
||||
scaleKey: '', // facets' scales used (above)
|
||||
lineColor: alpha('' + lineColor, 1),
|
||||
lineColor: alpha(lineColor ?? '#ffff', 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];
|
||||
const dispColors = xySeries.map((s): FieldColorValuesWithCache => {
|
||||
const cfg: FieldColorValuesWithCache = {
|
||||
index: [],
|
||||
getAll: () => [],
|
||||
getOne: () => -1,
|
||||
// cache for renderer, refreshed in prepData()
|
||||
values: [],
|
||||
hasAlpha: false,
|
||||
};
|
||||
|
||||
return f.y.map((yIndex, frameSeriesIndex) => {
|
||||
let xValues = fields[f.x[frameSeriesIndex]].values;
|
||||
let yValues = fields[f.y[frameSeriesIndex]].values;
|
||||
let sizeValues = f.size;
|
||||
const f = s.color.field;
|
||||
|
||||
if (!Array.isArray(sizeValues)) {
|
||||
sizeValues = Array(xValues.length).fill(sizeValues);
|
||||
}
|
||||
if (f != null) {
|
||||
Object.assign(cfg, fieldValueColors(f, theme));
|
||||
cfg.hasAlpha = cfg.index.some((v) => !(v as string).endsWith('ff'));
|
||||
}
|
||||
|
||||
return [xValues, yValues, sizeValues];
|
||||
});
|
||||
return cfg;
|
||||
});
|
||||
|
||||
function prepData(xySeries: XYSeries[]): FacetedData {
|
||||
// if (info.error || !data.length) {
|
||||
// return [null];
|
||||
// }
|
||||
|
||||
const { size: sizeRange, color: colorRange } = getGlobalRanges(xySeries);
|
||||
|
||||
xySeries.forEach((s, i) => {
|
||||
dispColors[i].values = dispColors[i].getAll(s.color.field?.values ?? [], colorRange.min, colorRange.max);
|
||||
});
|
||||
|
||||
return [null, ...seriesData];
|
||||
});
|
||||
*/
|
||||
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;
|
||||
|
||||
let vals = s.size.field.values;
|
||||
let minVal = sizeRange.min;
|
||||
let maxVal = sizeRange.max;
|
||||
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,
|
||||
Array(len).fill(s.color.fixed!), // TODO: fails for by value
|
||||
];
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
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];
|
||||
// }
|
||||
|
||||
const { size: sizeRange } = getGlobalRanges(xySeries);
|
||||
|
||||
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;
|
||||
|
||||
let vals = s.size.field.values;
|
||||
let minVal = sizeRange.min;
|
||||
let maxVal = sizeRange.max;
|
||||
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,
|
||||
Array(len).fill(s.color.fixed!), // TODO: fails for by value
|
||||
];
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
const getGlobalRanges = (xySeries: XYSeries[]) => {
|
||||
const ranges = {
|
||||
size: {
|
||||
@ -518,3 +545,145 @@ const getGlobalRanges = (xySeries: XYSeries[]) => {
|
||||
|
||||
return ranges;
|
||||
};
|
||||
|
||||
function getHex8Color(color: string, theme: GrafanaTheme2) {
|
||||
return tinycolor(theme.visualization.getColorByName(color)).toHex8String();
|
||||
}
|
||||
|
||||
interface FieldColorValues {
|
||||
index: unknown[];
|
||||
getOne: GetOneValue;
|
||||
getAll: GetAllValues;
|
||||
}
|
||||
interface FieldColorValuesWithCache extends FieldColorValues {
|
||||
values: number[];
|
||||
hasAlpha: boolean;
|
||||
}
|
||||
type GetAllValues = (values: unknown[], min?: number, max?: number) => number[];
|
||||
type GetOneValue = (value: unknown, min?: number, max?: number) => number;
|
||||
|
||||
/** compiler for values to palette color idxs (from thresholds, mappings, by-value gradients) */
|
||||
function fieldValueColors(f: Field, theme: GrafanaTheme2): FieldColorValues {
|
||||
let index: unknown[] = [];
|
||||
let getAll: GetAllValues = () => [];
|
||||
let getOne: GetOneValue = () => -1;
|
||||
|
||||
let conds = '';
|
||||
|
||||
// if any mappings exist, use them regardless of other settings
|
||||
if (f.config.mappings?.length ?? 0 > 0) {
|
||||
let mappings = f.config.mappings!;
|
||||
|
||||
for (let i = 0; i < mappings.length; i++) {
|
||||
let m = mappings[i];
|
||||
|
||||
if (m.type === MappingType.ValueToText) {
|
||||
for (let k in m.options) {
|
||||
let { color } = m.options[k];
|
||||
|
||||
if (color != null) {
|
||||
let rhs = f.type === FieldType.string ? JSON.stringify(k) : Number(k);
|
||||
conds += `v === ${rhs} ? ${index.length} : `;
|
||||
index.push(getHex8Color(color, theme));
|
||||
}
|
||||
}
|
||||
} else if (m.options.result.color != null) {
|
||||
let { color } = m.options.result;
|
||||
|
||||
if (m.type === MappingType.RangeToText) {
|
||||
let range = [];
|
||||
|
||||
if (m.options.from != null) {
|
||||
range.push(`v >= ${Number(m.options.from)}`);
|
||||
}
|
||||
|
||||
if (m.options.to != null) {
|
||||
range.push(`v <= ${Number(m.options.to)}`);
|
||||
}
|
||||
|
||||
if (range.length > 0) {
|
||||
conds += `${range.join(' && ')} ? ${index.length} : `;
|
||||
index.push(getHex8Color(color, theme));
|
||||
}
|
||||
} else if (m.type === MappingType.SpecialValue) {
|
||||
let spl = m.options.match;
|
||||
|
||||
if (spl === SpecialValueMatch.NaN) {
|
||||
conds += `isNaN(v)`;
|
||||
} else if (spl === SpecialValueMatch.NullAndNaN) {
|
||||
conds += `v == null || isNaN(v)`;
|
||||
} else {
|
||||
conds += `v ${
|
||||
spl === SpecialValueMatch.True
|
||||
? '=== true'
|
||||
: spl === SpecialValueMatch.False
|
||||
? '=== false'
|
||||
: spl === SpecialValueMatch.Null
|
||||
? '== null'
|
||||
: spl === SpecialValueMatch.Empty
|
||||
? '=== ""'
|
||||
: '== null'
|
||||
}`;
|
||||
}
|
||||
|
||||
conds += ` ? ${index.length} : `;
|
||||
index.push(getHex8Color(color, theme));
|
||||
} else if (m.type === MappingType.RegexToText) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conds += '-1'; // ?? what default here? null? FALLBACK_COLOR?
|
||||
} else if (f.config.color?.mode === FieldColorModeId.Thresholds) {
|
||||
if (f.config.thresholds?.mode === ThresholdsMode.Absolute) {
|
||||
let steps = f.config.thresholds.steps;
|
||||
let lasti = steps.length - 1;
|
||||
|
||||
for (let i = lasti; i > 0; i--) {
|
||||
conds += `v >= ${steps[i].value} ? ${i} : `;
|
||||
}
|
||||
|
||||
conds += '0';
|
||||
|
||||
index = steps.map((s) => getHex8Color(s.color, theme));
|
||||
} else {
|
||||
// TODO: percent thresholds?
|
||||
}
|
||||
} else if (f.config.color?.mode?.startsWith('continuous')) {
|
||||
let calc = getFieldColorModeForField(f).getCalculator(f, theme);
|
||||
|
||||
index = Array(32);
|
||||
|
||||
for (let i = 0; i < index.length; i++) {
|
||||
let pct = i / (index.length - 1);
|
||||
index[i] = getHex8Color(calc(pct, pct), theme);
|
||||
}
|
||||
|
||||
getAll = (vals, min, max) => valuesToFills(vals as number[], index as string[], min!, max!);
|
||||
}
|
||||
|
||||
if (conds !== '') {
|
||||
getOne = new Function('v', `return ${conds};`) as GetOneValue;
|
||||
|
||||
getAll = new Function(
|
||||
'vals',
|
||||
`
|
||||
let idxs = Array(vals.length);
|
||||
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
let v = vals[i];
|
||||
idxs[i] = ${conds};
|
||||
}
|
||||
|
||||
return idxs;
|
||||
`
|
||||
) as GetAllValues;
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
getOne,
|
||||
getAll,
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Field } from '@grafana/data';
|
||||
import * as common from '@grafana/schema';
|
||||
|
||||
import { PointShape } from './panelcfg.gen';
|
||||
|
||||
// import { SeriesMapping, XYSeriesConfig } from './panelcfg.gen';
|
||||
|
||||
// // panel save model
|
||||
@ -47,6 +49,9 @@ import * as common from '@grafana/schema';
|
||||
// materialized series (internal)
|
||||
export interface XYSeries {
|
||||
showPoints: common.VisibilityMode;
|
||||
pointShape: PointShape;
|
||||
pointStrokeWidth: number;
|
||||
fillOpacity: number;
|
||||
|
||||
showLine: boolean;
|
||||
lineWidth: number;
|
||||
|
@ -107,8 +107,12 @@ export function prepSeries(
|
||||
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) {
|
||||
if (field === x) {
|
||||
return;
|
||||
}
|
||||
|
||||
// in auto mode don't reuse already-mapped fields
|
||||
if (mapping === SeriesMapping.Auto && (field === color || field === size)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -129,11 +133,14 @@ export function prepSeries(
|
||||
},
|
||||
|
||||
showPoints: y.config.custom.show === XYShowMode.Lines ? VisibilityMode.Never : VisibilityMode.Always,
|
||||
pointShape: y.config.custom.pointShape,
|
||||
pointStrokeWidth: y.config.custom.pointStrokeWidth,
|
||||
fillOpacity: y.config.custom.fillOpacity,
|
||||
|
||||
showLine: y.config.custom.show !== XYShowMode.Points,
|
||||
lineWidth: y.config.custom.lineWidth ?? 2,
|
||||
lineStyle: y.config.custom.lineStyle,
|
||||
// lineColor: () => seriesColor,
|
||||
|
||||
x: {
|
||||
field: x!,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user