Skip to content

Commit

Permalink
Allow to set node radius in dataframe
Browse files Browse the repository at this point in the history
  • Loading branch information
piggito committed Sep 22, 2023
1 parent aadd5ba commit d2e0b54
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,4 @@ Optional fields:
| detail\_\_\* | string/number | Any field prefixed with `detail__` will be shown in the header of context menu when clicked on the node. Use `config.displayName` for more human readable label. |
| color | string/number | Can be used to specify a single color instead of using the `arc__` fields to specify color sections. It can be either a string which should then be an acceptable HTML color string or it can be a number in which case the behaviour depends on `field.config.color.mode` setting. This can be for example used to create gradient colors controlled by the field value. |
| icon | string | Name of the icon to show inside the node instead of the default stats. Only Grafana built in icons are allowed (see the available icons [here](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview)). |
| nodeRadius | number | Radius value in pixels. Used to manage node size. |
2 changes: 2 additions & 0 deletions packages/grafana-data/src/utils/nodeGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ export enum NodeGraphDataFrameFieldNames {

// Prefix for fields which will be shown in a context menu [nodes + edges]
detail = 'detail__',

nodeRadius = 'noderadius',
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ export function generateRandomNodes(count = 10) {
values: [],
type: FieldType.string,
},
[NodeGraphDataFrameFieldNames.nodeRadius]: {
values: [],
type: FieldType.number,
},
};

const nodeFrame = new MutableDataFrame({
Expand Down
12 changes: 10 additions & 2 deletions public/app/plugins/panel/nodeGraph/Edge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { MouseEvent, memo } from 'react';

import { nodeR } from './Node';
import { EdgeDatum, NodeDatum } from './types';
import { shortenLine } from './utils';

Expand All @@ -12,8 +13,14 @@ interface Props {
}
export const Edge = memo(function Edge(props: Props) {
const { edge, onClick, onMouseEnter, onMouseLeave, hovering } = props;

// Not great typing but after we do layout these properties are full objects not just references
const { source, target } = edge as { source: NodeDatum; target: NodeDatum };
const { source, target, sourceNodeRadius, targetNodeRadius } = edge as {
source: NodeDatum;
target: NodeDatum;
sourceNodeRadius: number;
targetNodeRadius: number;
};

// As the nodes have some radius we want edges to end outside of the node circle.
const line = shortenLine(
Expand All @@ -23,7 +30,8 @@ export const Edge = memo(function Edge(props: Props) {
x2: target.x!,
y2: target.y!,
},
90
sourceNodeRadius || nodeR,
targetNodeRadius || nodeR
);

return (
Expand Down
11 changes: 9 additions & 2 deletions public/app/plugins/panel/nodeGraph/EdgeLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { memo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';

import { nodeR } from './Node';
import { EdgeDatum, NodeDatum } from './types';
import { shortenLine } from './utils';

Expand All @@ -30,7 +31,12 @@ interface Props {
export const EdgeLabel = memo(function EdgeLabel(props: Props) {
const { edge } = props;
// Not great typing, but after we do layout these properties are full objects not just references
const { source, target } = edge as { source: NodeDatum; target: NodeDatum };
const { source, target, sourceNodeRadius, targetNodeRadius } = edge as {
source: NodeDatum;
target: NodeDatum;
sourceNodeRadius: number;
targetNodeRadius: number;
};

// As the nodes have some radius we want edges to end outside the node circle.
const line = shortenLine(
Expand All @@ -40,7 +46,8 @@ export const EdgeLabel = memo(function EdgeLabel(props: Props) {
x2: target.x!,
y2: target.y!,
},
90
sourceNodeRadius || nodeR,
targetNodeRadius || nodeR
);

const middle = {
Expand Down
16 changes: 16 additions & 0 deletions public/app/plugins/panel/nodeGraph/Node.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ describe('Node', () => {

expect(screen.getByTestId('node-icon-database')).toBeInTheDocument();
});

it('renders correct node radius', async () => {
render(
<svg>
<Node
node={{ ...nodeDatum, nodeRadius: { name: 'nodeRadius', values: [20], type: FieldType.number, config: {} } }}
onMouseEnter={() => {}}
onMouseLeave={() => {}}
onClick={() => {}}
hovering={'default'}
/>
</svg>
);

expect(screen.getByTestId('node-circle-1')).toHaveAttribute('r', '20');
});
});

const nodeDatum = {
Expand Down
32 changes: 21 additions & 11 deletions public/app/plugins/panel/nodeGraph/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { HoverState } from './NodeGraph';
import { NodeDatum } from './types';
import { statToString } from './utils';

const nodeR = 40;
export const nodeR = 40;

const getStyles = (theme: GrafanaTheme2, hovering: HoverState) => ({
mainGroup: css`
Expand Down Expand Up @@ -77,21 +77,30 @@ export const Node = memo(function Node(props: {
const theme = useTheme2();
const styles = getStyles(theme, hovering);
const isHovered = hovering === 'active';
const nodeRadius = node.nodeRadius?.values[node.dataFrameRowIndex] || nodeR;

if (!(node.x !== undefined && node.y !== undefined)) {
return null;
}

return (
<g data-node-id={node.id} className={styles.mainGroup} aria-label={`Node: ${node.title}`}>
<circle className={styles.mainCircle} r={nodeR} cx={node.x} cy={node.y} />
{isHovered && <circle className={styles.hoverCircle} r={nodeR - 3} cx={node.x} cy={node.y} strokeWidth={2} />}
<circle
data-testid={`node-circle-${node.id}`}
className={styles.mainCircle}
r={nodeRadius}
cx={node.x}
cy={node.y}
/>
{isHovered && (
<circle className={styles.hoverCircle} r={nodeRadius - 3} cx={node.x} cy={node.y} strokeWidth={2} />
)}
<ColorCircle node={node} />
<g className={styles.text} style={{ pointerEvents: 'none' }}>
<NodeContents node={node} hovering={hovering} />
<foreignObject
x={node.x - (isHovered ? 100 : 70)}
y={node.y + nodeR + 5}
y={node.y + nodeRadius + 5}
width={isHovered ? '200' : '140'}
height="40"
>
Expand All @@ -114,10 +123,10 @@ export const Node = memo(function Node(props: {
onClick(event, node);
}}
className={styles.clickTarget}
x={node.x - nodeR - 5}
y={node.y - nodeR - 5}
width={nodeR * 2 + 10}
height={nodeR * 2 + 50}
x={node.x - nodeRadius - 5}
y={node.y - nodeRadius - 5}
width={nodeRadius * 2 + 10}
height={nodeRadius * 2 + 50}
/>
</g>
);
Expand Down Expand Up @@ -162,6 +171,7 @@ function ColorCircle(props: { node: NodeDatum }) {
const { node } = props;
const fullStat = node.arcSections.find((s) => s.values[node.dataFrameRowIndex] >= 1);
const theme = useTheme2();
const nodeRadius = node.nodeRadius?.values[node.dataFrameRowIndex] || nodeR;

if (fullStat) {
// Doing arc with path does not work well so it's better to just do a circle in that case
Expand All @@ -170,7 +180,7 @@ function ColorCircle(props: { node: NodeDatum }) {
fill="none"
stroke={theme.visualization.getColorByName(fullStat.config.color?.fixedColor || '')}
strokeWidth={2}
r={nodeR}
r={nodeRadius}
cx={node.x}
cy={node.y}
/>
Expand All @@ -185,7 +195,7 @@ function ColorCircle(props: { node: NodeDatum }) {
fill="none"
stroke={node.color ? getColor(node.color, node.dataFrameRowIndex, theme) : 'gray'}
strokeWidth={2}
r={nodeR}
r={nodeRadius}
cx={node.x}
cy={node.y}
/>
Expand All @@ -203,7 +213,7 @@ function ColorCircle(props: { node: NodeDatum }) {
const el = (
<ArcSection
key={index}
r={nodeR}
r={nodeRadius}
x={node.x!}
y={node.y!}
startPercent={acc.percent}
Expand Down
2 changes: 2 additions & 0 deletions public/app/plugins/panel/nodeGraph/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,7 @@ function makeEdge(source: number, target: number): EdgeDatum {
mainStat: '',
secondaryStat: '',
dataFrameRowIndex: 0,
sourceNodeRadius: 40,
targetNodeRadius: 40,
};
}
3 changes: 3 additions & 0 deletions public/app/plugins/panel/nodeGraph/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type NodeDatum = SimulationNodeDatum & {
arcSections: Field[];
color?: Field;
icon?: IconName;
nodeRadius?: Field;
};

export type NodeDatumFromEdge = NodeDatum & { mainStatNumeric?: number; secondaryStatNumeric?: number };
Expand All @@ -31,6 +32,8 @@ export type EdgeDatum = LinkDatum & {
mainStat: string;
secondaryStat: string;
dataFrameRowIndex: number;
sourceNodeRadius: number;
targetNodeRadius: number;
};

// After layout is run D3 will change the string IDs for actual references to the nodes.
Expand Down
10 changes: 10 additions & 0 deletions public/app/plugins/panel/nodeGraph/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ describe('processNodes', () => {
{ name: 'SUBTITLE', type: FieldType.string, values: ['subTitle'] },
{ name: 'mainstat', type: FieldType.string, values: ['mainStat'] },
{ name: 'seconDarysTat', type: FieldType.string, values: ['secondaryStat'] },
{ name: 'nodeRadius', type: FieldType.number, values: [20] },
],
});

Expand Down Expand Up @@ -312,6 +313,13 @@ function makeNodeDatum(options: Partial<NodeDatum> = {}) {
subTitle: 'service',
title: 'service:0',
icon: 'database',
nodeRadius: {
config: {},
index: 9,
name: 'noderadius',
type: 'number',
values: [40, 40, 40],
},
...options,
};
}
Expand All @@ -324,6 +332,8 @@ function makeEdgeDatum(id: string, index: number, mainStat = '', secondaryStat =
secondaryStat,
source: id.split('--')[0],
target: id.split('--')[1],
sourceNodeRadius: 40,
targetNodeRadius: 40,
};
}

Expand Down

0 comments on commit d2e0b54

Please sign in to comment.