Files
Rob 677b765dab NodeGraph: Edge color and stroke-dasharray support (#83855)
* Adds color and stroke-dasharray support for node graph edges

Adds support for providing color, highlighted color, and visual display of node graph edges as dashed lines via stroke-dasharray.

* Updates node graph documentation

* Updates documentation

Adds default for `highlightedColor`

* Update docs/sources/panels-visualizations/visualizations/node-graph/index.md

Co-authored-by: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com>

* Update packages/grafana-data/src/utils/nodeGraph.ts

Co-authored-by: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com>

* Update docs/sources/panels-visualizations/visualizations/node-graph/index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

* Removes highlightedColor; deprecates highlighted

Per [request](https://github.com/grafana/grafana/pull/83855#issuecomment-1999810826), deprecates `highlighted` in code and documentation, and removes `highlightedColor` as an additional property. `highlighted` will continue to be supported in its original state (makes the edge red), but is superseded if `color` is provided.

* Update types.ts

Missed a file in my last commit. Removes `highlightedColor` and deprecates `highlighted`.

* Add test scenario in test data source

---------

Co-authored-by: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com>
Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
2024-03-18 16:26:22 +01:00

316 lines
11 KiB
TypeScript

import { randomLcg } from 'd3-random';
import {
FieldColorModeId,
FieldDTO,
FieldType,
MutableDataFrame,
NodeGraphDataFrameFieldNames,
DataFrame,
addRow,
} from '@grafana/data';
import * as serviceMapResponseSmall from './testData/serviceMapResponse';
import * as serviceMapResponsMedium from './testData/serviceMapResponseMedium';
export function generateRandomNodes(count = 10, seed?: number) {
const nodes = [];
const edges: string[] = [];
const rand = randomLcg(seed);
const root = {
id: 'root',
title: 'root',
subTitle: 'client',
success: 1,
error: 0,
stat1: Math.random(),
stat2: Math.random(),
edges,
};
nodes.push(root);
const nodesWithoutMaxEdges = [root];
const maxEdges = 3;
for (let i = 1; i < count; i++) {
const node = makeRandomNode(i);
nodes.push(node);
const sourceIndex = Math.floor(rand() * Math.floor(nodesWithoutMaxEdges.length - 1));
const source = nodesWithoutMaxEdges[sourceIndex];
source.edges.push(node.id);
if (source.edges.length >= maxEdges) {
nodesWithoutMaxEdges.splice(sourceIndex, 1);
}
nodesWithoutMaxEdges.push(node);
}
// Add some random edges to create possible cycle
const additionalEdges = Math.floor(count / 2);
for (let i = 0; i <= additionalEdges; i++) {
const sourceIndex = Math.floor(rand() * Math.floor(nodes.length - 1));
const targetIndex = Math.floor(rand() * Math.floor(nodes.length - 1));
if (sourceIndex === targetIndex || nodes[sourceIndex].id === '0' || nodes[targetIndex].id === '0') {
continue;
}
nodes[sourceIndex].edges.push(nodes[targetIndex].id);
}
const { nodesFields, nodesFrame, edgesFrame } = makeDataFrames();
const edgesSet = new Set();
for (const node of nodes) {
nodesFields.id.values.push(node.id);
nodesFields.title.values.push(node.title);
nodesFields[NodeGraphDataFrameFieldNames.subTitle].values.push(node.subTitle);
nodesFields[NodeGraphDataFrameFieldNames.mainStat].values.push(node.stat1);
nodesFields[NodeGraphDataFrameFieldNames.secondaryStat].values.push(node.stat2);
nodesFields.arc__success.values.push(node.success);
nodesFields.arc__errors.values.push(node.error);
const rnd = Math.random();
nodesFields[NodeGraphDataFrameFieldNames.icon].values.push(rnd > 0.9 ? 'database' : rnd < 0.1 ? 'cloud' : '');
nodesFields[NodeGraphDataFrameFieldNames.nodeRadius].values.push(Math.max(rnd * 100, 30)); // ensure a minimum radius of 30 or icons will not fit well in the node
nodesFields[NodeGraphDataFrameFieldNames.highlighted].values.push(Math.random() > 0.5);
for (const edge of node.edges) {
const id = `${node.id}--${edge}`;
// We can have duplicate edges when we added some more by random
if (edgesSet.has(id)) {
continue;
}
edgesSet.add(id);
edgesFrame.fields[0].values.push(`${node.id}--${edge}`);
edgesFrame.fields[1].values.push(node.id);
edgesFrame.fields[2].values.push(edge);
edgesFrame.fields[3].values.push(Math.random() * 100);
edgesFrame.fields[4].values.push(Math.random() > 0.5);
edgesFrame.fields[5].values.push(Math.ceil(Math.random() * 15));
}
}
edgesFrame.length = edgesFrame.fields[0].values.length;
return [nodesFrame, edgesFrame];
}
function makeRandomNode(index: number) {
const success = Math.random();
const error = 1 - success;
return {
id: `service:${index}`,
title: `service:${index}`,
subTitle: 'service',
success,
error,
stat1: Math.random(),
stat2: Math.random(),
edges: [],
highlighted: Math.random() > 0.5,
};
}
export function savedNodesResponse(size: 'small' | 'medium'): [DataFrame, DataFrame] {
const response = size === 'small' ? serviceMapResponseSmall : serviceMapResponsMedium;
return [new MutableDataFrame(response.nodes), new MutableDataFrame(response.edges)];
}
// Generates node graph data but only returns the edges
export function generateRandomEdges(count = 10, seed = 1) {
return generateRandomNodes(count, seed)[1];
}
function makeDataFrames(): {
nodesFrame: DataFrame;
edgesFrame: DataFrame;
nodesFields: Record<string, Omit<FieldDTO, 'name'> & { values: unknown[] }>;
} {
const nodesFields: Record<string, Omit<FieldDTO, 'name'> & { values: unknown[] }> = {
[NodeGraphDataFrameFieldNames.id]: {
values: [],
type: FieldType.string,
config: {
links: [
{
title: 'test data link',
url: '',
internal: {
query: { scenarioId: 'logs', alias: 'from service graph', stringInput: 'tes' },
datasourceUid: 'gdev-testdata',
datasourceName: 'gdev-testdata',
},
},
],
},
},
[NodeGraphDataFrameFieldNames.title]: {
values: [],
type: FieldType.string,
},
[NodeGraphDataFrameFieldNames.subTitle]: {
values: [],
type: FieldType.string,
},
[NodeGraphDataFrameFieldNames.mainStat]: {
values: [],
type: FieldType.number,
config: { displayName: 'Transactions per second' },
},
[NodeGraphDataFrameFieldNames.secondaryStat]: {
values: [],
type: FieldType.number,
config: { displayName: 'Average duration' },
},
[NodeGraphDataFrameFieldNames.arc + 'success']: {
values: [],
type: FieldType.number,
config: { color: { fixedColor: 'green', mode: FieldColorModeId.Fixed }, displayName: 'Success' },
},
[NodeGraphDataFrameFieldNames.arc + 'errors']: {
values: [],
type: FieldType.number,
config: { color: { fixedColor: 'red', mode: FieldColorModeId.Fixed }, displayName: 'Errors' },
},
[NodeGraphDataFrameFieldNames.icon]: {
values: [],
type: FieldType.string,
},
[NodeGraphDataFrameFieldNames.nodeRadius]: {
values: [],
type: FieldType.number,
},
[NodeGraphDataFrameFieldNames.highlighted]: {
values: [],
type: FieldType.boolean,
},
[NodeGraphDataFrameFieldNames.detail + 'test_value']: {
values: [],
config: {
displayName: 'Test value',
},
type: FieldType.number,
},
};
const nodesFrame = new MutableDataFrame({
name: 'nodes',
fields: Object.keys(nodesFields).map((key) => ({
...nodesFields[key],
name: key,
})),
meta: { preferredVisualisationType: 'nodeGraph' },
});
const edgesFrame: DataFrame = {
name: 'edges',
fields: [
{ name: NodeGraphDataFrameFieldNames.id, values: [], type: FieldType.string, config: {} },
{ name: NodeGraphDataFrameFieldNames.source, values: [], type: FieldType.string, config: {} },
{ name: NodeGraphDataFrameFieldNames.target, values: [], type: FieldType.string, config: {} },
{ name: NodeGraphDataFrameFieldNames.mainStat, values: [], type: FieldType.number, config: {} },
{ name: NodeGraphDataFrameFieldNames.highlighted, values: [], type: FieldType.boolean, config: {} },
{ name: NodeGraphDataFrameFieldNames.thickness, values: [], type: FieldType.number, config: {} },
{ name: NodeGraphDataFrameFieldNames.color, values: [], type: FieldType.string, config: {} },
{ name: NodeGraphDataFrameFieldNames.strokeDasharray, values: [], type: FieldType.string, config: {} },
],
meta: { preferredVisualisationType: 'nodeGraph' },
length: 0,
};
return { nodesFrame, edgesFrame, nodesFields };
}
export function generateShowcaseData() {
const { nodesFrame, edgesFrame } = makeDataFrames();
addRow(nodesFrame, {
[NodeGraphDataFrameFieldNames.id]: 'root',
[NodeGraphDataFrameFieldNames.title]: 'root',
[NodeGraphDataFrameFieldNames.subTitle]: 'client',
[NodeGraphDataFrameFieldNames.mainStat]: 1234,
[NodeGraphDataFrameFieldNames.secondaryStat]: 5678,
arc__success: 0.5,
arc__errors: 0.5,
[NodeGraphDataFrameFieldNames.icon]: '',
[NodeGraphDataFrameFieldNames.nodeRadius]: 40,
[NodeGraphDataFrameFieldNames.highlighted]: false,
[NodeGraphDataFrameFieldNames.detail + 'test_value']: 1,
});
addRow(nodesFrame, {
[NodeGraphDataFrameFieldNames.id]: 'app_service',
[NodeGraphDataFrameFieldNames.title]: 'app service',
[NodeGraphDataFrameFieldNames.subTitle]: 'with icon',
[NodeGraphDataFrameFieldNames.mainStat]: 1.2,
[NodeGraphDataFrameFieldNames.secondaryStat]: 2.3,
arc__success: 1,
arc__errors: 0,
[NodeGraphDataFrameFieldNames.icon]: 'apps',
[NodeGraphDataFrameFieldNames.nodeRadius]: 40,
[NodeGraphDataFrameFieldNames.highlighted]: false,
[NodeGraphDataFrameFieldNames.detail + 'test_value']: 42,
});
addRow(edgesFrame, {
[NodeGraphDataFrameFieldNames.id]: 'root-app_service',
[NodeGraphDataFrameFieldNames.source]: 'root',
[NodeGraphDataFrameFieldNames.target]: 'app_service',
[NodeGraphDataFrameFieldNames.mainStat]: 3.4,
[NodeGraphDataFrameFieldNames.secondaryStat]: 4.5,
[NodeGraphDataFrameFieldNames.thickness]: 4,
[NodeGraphDataFrameFieldNames.color]: '',
[NodeGraphDataFrameFieldNames.strokeDasharray]: '',
});
addRow(nodesFrame, {
[NodeGraphDataFrameFieldNames.id]: 'auth_service',
[NodeGraphDataFrameFieldNames.title]: 'auth service',
[NodeGraphDataFrameFieldNames.subTitle]: 'highlighted',
[NodeGraphDataFrameFieldNames.mainStat]: 3.4,
[NodeGraphDataFrameFieldNames.secondaryStat]: 4.5,
arc__success: 0,
arc__errors: 1,
[NodeGraphDataFrameFieldNames.icon]: '',
[NodeGraphDataFrameFieldNames.nodeRadius]: 40,
[NodeGraphDataFrameFieldNames.highlighted]: true,
});
addRow(edgesFrame, {
[NodeGraphDataFrameFieldNames.id]: 'root-auth_service',
[NodeGraphDataFrameFieldNames.source]: 'root',
[NodeGraphDataFrameFieldNames.target]: 'auth_service',
[NodeGraphDataFrameFieldNames.mainStat]: 113.4,
[NodeGraphDataFrameFieldNames.secondaryStat]: 4.511,
[NodeGraphDataFrameFieldNames.thickness]: 8,
[NodeGraphDataFrameFieldNames.color]: 'red',
[NodeGraphDataFrameFieldNames.strokeDasharray]: '',
});
addRow(nodesFrame, {
[NodeGraphDataFrameFieldNames.id]: 'db',
[NodeGraphDataFrameFieldNames.title]: 'db',
[NodeGraphDataFrameFieldNames.subTitle]: 'bigger size',
[NodeGraphDataFrameFieldNames.mainStat]: 9876.123,
[NodeGraphDataFrameFieldNames.secondaryStat]: 123.9876,
arc__success: 0.9,
arc__errors: 0.1,
[NodeGraphDataFrameFieldNames.icon]: 'database',
[NodeGraphDataFrameFieldNames.nodeRadius]: 60,
[NodeGraphDataFrameFieldNames.highlighted]: false,
[NodeGraphDataFrameFieldNames.detail + 'test_value']: 1357,
});
addRow(edgesFrame, {
[NodeGraphDataFrameFieldNames.id]: 'auth_service-db',
[NodeGraphDataFrameFieldNames.source]: 'auth_service',
[NodeGraphDataFrameFieldNames.target]: 'db',
[NodeGraphDataFrameFieldNames.mainStat]: 1139.4,
[NodeGraphDataFrameFieldNames.secondaryStat]: 477.511,
[NodeGraphDataFrameFieldNames.thickness]: 2,
[NodeGraphDataFrameFieldNames.color]: 'blue',
[NodeGraphDataFrameFieldNames.strokeDasharray]: '2 2',
});
return [nodesFrame, edgesFrame];
}