XYChart2: Implement color-by-field (#88467)

This commit is contained in:
Leon Sorokin 2024-06-03 11:53:59 -05:00 committed by GitHub
parent e419c76842
commit 87cafbf9af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2007 additions and 149 deletions

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -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]: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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![frameSeriesIndex](frames[i]);
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,
};
}

View File

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

View File

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