plotly_graph_fl()

适用于:✅Azure 数据资源管理器

使用图形数据中的 Plotly 创建交互式图形可视化效果。

该函数 plotly_graph_fl()用户定义的函数(UDF), 可用于自定义 绘图 模板以创建交互式图形可视化效果。

先决条件

  • 必须在 群集上启用 Python 插件。 这是函数中使用的内联 Python 所必需的。

该函数接受具有可自定义列映射和外观设置的单独节点和边缘表,并返回包含 绘图 JSON 的单个单元格表。 (可选)可以在 Azure 数据资源管理器仪表板 磁贴中呈现数据。 有关详细信息,请参阅 Plotly (预览版)。

该函数可用于可视化网络关系、网络安全攻击路径、组织结构或任何可表示为图形的连接数据。 生成的可视化效果包括悬停信息、基于连接的节点大小以及使用 Python 的 NetworkX 库进行图形布局计算的可自定义配色方案,以及用于呈现交互式可视化效果的 Plotly。

Syntax

plotly_graph_fl( edgesnodes, [node_id_column], [source_id_column], [target_id_column], [colorscale_id], [diagram_title])

详细了解语法约定

参数

Name 类型 必选 Description
边缘 表格式的 ✔️ 包含具有源节点和目标节点标识符的边缘数据的表。
节点 表格式的 ✔️ 包含具有唯一节点标识符和可选属性的节点数据的表。
node_id_column string 包含节点表中唯一节点标识符的列的名称。 默认值为 "nodeId"
source_id_column string 列的名称,其中包含边缘表中的源节点标识符。 默认值为 "sourceId"
target_id_column string 列的名称,其中包含边缘表中的目标节点标识符。 默认值为 "targetId"
colorscale_id string 要用于节点颜色的绘图色阶。 可用选项包括“Viridis”、“等离子”、“Inferno”、“Magma”、“Cividis”、“YlGnBu”等。默认值为 "Cividis".
diagram_title string 要显示在图形可视化效果顶部的标题。 默认值为 "Simple Graph for educational purposes"

函数定义

可以通过将函数代码嵌入为查询定义的函数,或将其创建为数据库中的存储函数来定义函数,如下所示:

使用以下 let 语句定义函数。 不需要任何权限。

重要

let 语句不能自行运行。 它必须后跟 表格表达式语句。 若要运行工作示例 plotly_graph_fl(),请参阅 示例

let plotly_graph_fl = (
    edges:(*), nodes:(*), 
    node_id_column:string="nodeId",
    source_id_column:string="sourceId", target_id_column:string="targetId",
    colorscale_id:string="Cividis", diagram_title:string="Simple Graph for educational purposes") {
let pythonCodeBlueprint = ```
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import networkx as nx

# Configuration constants from parameters with defaults
NODE_OR_EDGE_COLUMN = kargs.get("node_or_edge_column", "tableName")
NODE_ROW = kargs.get("node_row", "V")
NODE_ID_COLUMN = kargs.get("node_id_column", "nodeId")
EDGE_ROW = kargs.get("edge_row", "E")
SOURCE_ID_COLUMN = kargs.get("source_id_column", "sourceId")
TARGET_ID_COLUMN = kargs.get("target_id_column", "targetId")
COLORSCALE_ID = kargs.get("colorscale_id", "YlGnBu")  # see https://plotly.com/python/builtin-colorscales/
DIAGRAM_TITLE = kargs.get("diagram_title", "Network Graph Visualization")


def is_valid_value(value):
    """
    Check if a value is valid (not None, not empty string, not all NaN).
    
    Args:
        value: Value to check
        
    Returns:
        bool: True if value is valid, False otherwise
    """
    if value is None or (isinstance(value, str) and value == ''):
        return False
    
    # Handle arrays/series safely
    if hasattr(value, '__len__') and not isinstance(value, str):
        try:
            na_result = pd.isna(value)
            return not (hasattr(na_result, 'all') and na_result.all()) and not na_result
        except (TypeError, ValueError):
            return True
    
    return not pd.isna(value)


def create_properties_dict(row):
    """
    Create a properties dictionary from a pandas Series, filtering out invalid values.
    
    Args:
        row (pd.Series): Row containing properties
        
    Returns:
        dict: Filtered properties dictionary
    """
    return {k: v for k, v in row.items() if is_valid_value(v)}


def build_graph(dataframe):
    """
    Build a NetworkX graph from a dataframe containing nodes and edges.
    
    Args:
        dataframe (pd.DataFrame): DataFrame with node and edge data
        
    Returns:
        nx.Graph: Constructed graph
    """
    graph = nx.Graph()
    
    # Separate and process nodes and edges efficiently
    nodes_df = dataframe[dataframe[NODE_OR_EDGE_COLUMN] == NODE_ROW]
    edges_df = dataframe[dataframe[NODE_OR_EDGE_COLUMN] == EDGE_ROW]
    
    # Add nodes
    if not nodes_df.empty:
        for _, row in nodes_df.iterrows():
            graph.add_node(row[NODE_ID_COLUMN], size=1, 
                          properties=create_properties_dict(row))
    
    # Add edges
    if not edges_df.empty:
        for _, row in edges_df.iterrows():
            graph.add_edge(row[SOURCE_ID_COLUMN], row[TARGET_ID_COLUMN], 
                          weight=1, properties=create_properties_dict(row))
    
    return graph


def create_edge_traces(graph, positions):
    """
    Create Plotly traces for graph edges and their midpoint markers.
    
    Args:
        graph (nx.Graph): NetworkX graph
        positions (dict): Node positions from layout algorithm
        
    Returns:
        tuple: (edge_trace, mnode_trace) - Plotly scatter traces
    """
    edge_x, edge_y = [], []
    mnode_x, mnode_y, mnode_txt = [], [], []
    
    # Process all edges efficiently
    for source, target, edge_data in graph.edges(data=True):
        x0, y0 = positions[source]
        x1, y1 = positions[target]
        
        # Add edge coordinates (with None separator for line breaks)
        edge_x.extend([x0, x1, None])
        edge_y.extend([y0, y1, None])
        
        # Create hover text for edge midpoint
        properties = edge_data.get('properties', {})
        valid_props = {k: v for k, v in properties.items() if is_valid_value(v)}
        
        prop_text = '<br>'.join(f'{key}: {value}' for key, value in valid_props.items())
        text = f'source: {source} destination: {target}<br>Properties: {prop_text}'
        
        # Add midpoint coordinates and text
        mnode_x.append((x0 + x1) * 0.5)
        mnode_y.append((y0 + y1) * 0.5)
        mnode_txt.append(text)
    
    # Create traces
    edge_trace = go.Scatter(
        x=edge_x, y=edge_y,
        line=dict(width=0.5, color='#888'),
        hoverinfo='none', mode='lines'
    )
    
    mnode_trace = go.Scatter(
        x=mnode_x, y=mnode_y, text=mnode_txt,
        mode="markers", hoverinfo='text', opacity=0.5,
        marker=dict(color='LightSkyBlue', size=2, line_width=1)
    )
    
    return edge_trace, mnode_trace


def create_node_trace(graph, positions):
    """
    Create Plotly trace for graph nodes.
    
    Args:
        graph (nx.Graph): NetworkX graph
        positions (dict): Node positions from layout algorithm
        
    Returns:
        go.Scatter: Plotly scatter trace for nodes
    """
    nodes_list = list(graph.nodes())
    num_nodes = len(nodes_list)
    inv_num_nodes = 1.0 / num_nodes if num_nodes > 0 else 0
    
    # Extract coordinates and calculate metrics
    node_x = [positions[node][0] for node in nodes_list]
    node_y = [positions[node][1] for node in nodes_list]
    
    node_adjacencies = []
    node_text = []
    node_sizes = []
    
    for node in nodes_list:
        num_connections = len(graph[node])
        node_adjacencies.append(num_connections)
        node_sizes.append(10 + num_connections * inv_num_nodes)
        
        # Create hover text
        node_properties = graph.nodes[node].get('properties', {})
        valid_props = {k: v for k, v in node_properties.items() if is_valid_value(v)}
        
        prop_text = '<br>'.join(f'{key}: {value}' for key, value in valid_props.items())
        text = f'# of connections: {num_connections}<br>Properties: {prop_text}'
        node_text.append(text)
    
    return go.Scatter(
        x=node_x, y=node_y, text=node_text,
        mode='markers', hoverinfo='text',
        marker=dict(
            colorscale=COLORSCALE_ID,
            color=node_adjacencies, size=node_sizes,
            line=dict(width=2, color='#888')
        )
    )


def create_plotly_figure(graph):
    """
    Create a complete Plotly figure from a NetworkX graph.
    
    Args:
        graph (nx.Graph): NetworkX graph to visualize
        
    Returns:
        go.Figure: Complete Plotly figure
    """
    # Calculate layout
    positions = nx.layout.spring_layout(graph)
    
    # Create traces
    edge_trace, mnode_trace = create_edge_traces(graph, positions)
    node_trace = create_node_trace(graph, positions)
    
    # Create figure
    return go.Figure(
        data=[edge_trace, node_trace, mnode_trace],
        layout=go.Layout(
            title=f'<br>{DIAGRAM_TITLE}<br>',
            showlegend=False, hovermode='closest',
            margin=dict(b=20, l=5, r=5, t=40),
            annotations=[dict(
                text="Created using plotly, networkx and the python plugin of Kusto",
                showarrow=False, xref="paper", yref="paper",
                x=0.005, y=-0.002
            )],
            xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
            yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
        )
    )


# Main execution
graph = build_graph(df)
figure = create_plotly_figure(graph)
plotly_obj = figure.to_json()
result = pd.DataFrame(data=[plotly_obj], columns=['plotly'])
```;
let E = view() { edges };
let V = view() { nodes };
union withsource=tableName E, V
| evaluate python(
//
typeof(plotly:string),
pythonCodeBlueprint,
// Parameter configuration for graph visualization
// Customize these values to adapt the visualization to your data schema and preferences
bag_pack(
    // Data schema mapping - configure these based on your data structure
    'node_or_edge_column', 'tableName',    // Column that distinguishes nodes from edges (source table name)
    'node_row', 'V',                       // Value in discriminator column identifying node records
    'node_id_column', node_id_column,            // Column containing unique node identifiers
    'edge_row', 'E',                       // Value in discriminator column identifying edge records
    'source_id_column', source_id_column,        // Column containing source node IDs for edges
    'target_id_column', target_id_column,        // Column containing target node IDs for edges
    // Visualization appearance settings
    'colorscale_id', colorscale_id,            // Plotly colorscale: YlGnBu, Viridis, Plasma, Inferno, Magma, Cividis, etc.
    'diagram_title', diagram_title    // Title displayed at the top of the graph visualization
))
};
// Write your query to use the function here.

Example

若要使用查询定义的函数,请调用嵌入的函数定义之后。

let plotly_graph_fl = (
    edges:(*), nodes:(*), 
    node_id_column:string="nodeId",
    source_id_column:string="sourceId", target_id_column:string="targetId",
    colorscale_id:string="Cividis", diagram_title:string="Simple Graph for educational purposes") {
let pythonCodeBlueprint = ```
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import networkx as nx

# Configuration constants from parameters with defaults
NODE_OR_EDGE_COLUMN = kargs.get("node_or_edge_column", "tableName")
NODE_ROW = kargs.get("node_row", "V")
NODE_ID_COLUMN = kargs.get("node_id_column", "nodeId")
EDGE_ROW = kargs.get("edge_row", "E")
SOURCE_ID_COLUMN = kargs.get("source_id_column", "sourceId")
TARGET_ID_COLUMN = kargs.get("target_id_column", "targetId")
COLORSCALE_ID = kargs.get("colorscale_id", "YlGnBu")  # see https://plotly.com/python/builtin-colorscales/
DIAGRAM_TITLE = kargs.get("diagram_title", "Network Graph Visualization")


def is_valid_value(value):
    """
    Check if a value is valid (not None, not empty string, not all NaN).
    
    Args:
        value: Value to check
        
    Returns:
        bool: True if value is valid, False otherwise
    """
    if value is None or (isinstance(value, str) and value == ''):
        return False
    
    # Handle arrays/series safely
    if hasattr(value, '__len__') and not isinstance(value, str):
        try:
            na_result = pd.isna(value)
            return not (hasattr(na_result, 'all') and na_result.all()) and not na_result
        except (TypeError, ValueError):
            return True
    
    return not pd.isna(value)


def create_properties_dict(row):
    """
    Create a properties dictionary from a pandas Series, filtering out invalid values.
    
    Args:
        row (pd.Series): Row containing properties
        
    Returns:
        dict: Filtered properties dictionary
    """
    return {k: v for k, v in row.items() if is_valid_value(v)}


def build_graph(dataframe):
    """
    Build a NetworkX graph from a dataframe containing nodes and edges.
    
    Args:
        dataframe (pd.DataFrame): DataFrame with node and edge data
        
    Returns:
        nx.Graph: Constructed graph
    """
    graph = nx.Graph()
    
    # Separate and process nodes and edges efficiently
    nodes_df = dataframe[dataframe[NODE_OR_EDGE_COLUMN] == NODE_ROW]
    edges_df = dataframe[dataframe[NODE_OR_EDGE_COLUMN] == EDGE_ROW]
    
    # Add nodes
    if not nodes_df.empty:
        for _, row in nodes_df.iterrows():
            graph.add_node(row[NODE_ID_COLUMN], size=1, 
                          properties=create_properties_dict(row))
    
    # Add edges
    if not edges_df.empty:
        for _, row in edges_df.iterrows():
            graph.add_edge(row[SOURCE_ID_COLUMN], row[TARGET_ID_COLUMN], 
                          weight=1, properties=create_properties_dict(row))
    
    return graph


def create_edge_traces(graph, positions):
    """
    Create Plotly traces for graph edges and their midpoint markers.
    
    Args:
        graph (nx.Graph): NetworkX graph
        positions (dict): Node positions from layout algorithm
        
    Returns:
        tuple: (edge_trace, mnode_trace) - Plotly scatter traces
    """
    edge_x, edge_y = [], []
    mnode_x, mnode_y, mnode_txt = [], [], []
    
    # Process all edges efficiently
    for source, target, edge_data in graph.edges(data=True):
        x0, y0 = positions[source]
        x1, y1 = positions[target]
        
        # Add edge coordinates (with None separator for line breaks)
        edge_x.extend([x0, x1, None])
        edge_y.extend([y0, y1, None])
        
        # Create hover text for edge midpoint
        properties = edge_data.get('properties', {})
        valid_props = {k: v for k, v in properties.items() if is_valid_value(v)}
        
        prop_text = '<br>'.join(f'{key}: {value}' for key, value in valid_props.items())
        text = f'source: {source} destination: {target}<br>Properties: {prop_text}'
        
        # Add midpoint coordinates and text
        mnode_x.append((x0 + x1) * 0.5)
        mnode_y.append((y0 + y1) * 0.5)
        mnode_txt.append(text)
    
    # Create traces
    edge_trace = go.Scatter(
        x=edge_x, y=edge_y,
        line=dict(width=0.5, color='#888'),
        hoverinfo='none', mode='lines'
    )
    
    mnode_trace = go.Scatter(
        x=mnode_x, y=mnode_y, text=mnode_txt,
        mode="markers", hoverinfo='text', opacity=0.5,
        marker=dict(color='LightSkyBlue', size=2, line_width=1)
    )
    
    return edge_trace, mnode_trace


def create_node_trace(graph, positions):
    """
    Create Plotly trace for graph nodes.
    
    Args:
        graph (nx.Graph): NetworkX graph
        positions (dict): Node positions from layout algorithm
        
    Returns:
        go.Scatter: Plotly scatter trace for nodes
    """
    nodes_list = list(graph.nodes())
    num_nodes = len(nodes_list)
    inv_num_nodes = 1.0 / num_nodes if num_nodes > 0 else 0
    
    # Extract coordinates and calculate metrics
    node_x = [positions[node][0] for node in nodes_list]
    node_y = [positions[node][1] for node in nodes_list]
    
    node_adjacencies = []
    node_text = []
    node_sizes = []
    
    for node in nodes_list:
        num_connections = len(graph[node])
        node_adjacencies.append(num_connections)
        node_sizes.append(10 + num_connections * inv_num_nodes)
        
        # Create hover text
        node_properties = graph.nodes[node].get('properties', {})
        valid_props = {k: v for k, v in node_properties.items() if is_valid_value(v)}
        
        prop_text = '<br>'.join(f'{key}: {value}' for key, value in valid_props.items())
        text = f'# of connections: {num_connections}<br>Properties: {prop_text}'
        node_text.append(text)
    
    return go.Scatter(
        x=node_x, y=node_y, text=node_text,
        mode='markers', hoverinfo='text',
        marker=dict(
            colorscale=COLORSCALE_ID,
            color=node_adjacencies, size=node_sizes,
            line=dict(width=2, color='#888')
        )
    )


def create_plotly_figure(graph):
    """
    Create a complete Plotly figure from a NetworkX graph.
    
    Args:
        graph (nx.Graph): NetworkX graph to visualize
        
    Returns:
        go.Figure: Complete Plotly figure
    """
    # Calculate layout
    positions = nx.layout.spring_layout(graph)
    
    # Create traces
    edge_trace, mnode_trace = create_edge_traces(graph, positions)
    node_trace = create_node_trace(graph, positions)
    
    # Create figure
    return go.Figure(
        data=[edge_trace, node_trace, mnode_trace],
        layout=go.Layout(
            title=f'<br>{DIAGRAM_TITLE}<br>',
            showlegend=False, hovermode='closest',
            margin=dict(b=20, l=5, r=5, t=40),
            annotations=[dict(
                text="Created using plotly, networkx and the python plugin of Kusto",
                showarrow=False, xref="paper", yref="paper",
                x=0.005, y=-0.002
            )],
            xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
            yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
        )
    )


# Main execution
graph = build_graph(df)
figure = create_plotly_figure(graph)
plotly_obj = figure.to_json()
result = pd.DataFrame(data=[plotly_obj], columns=['plotly'])
```;
let E = view() { edges };
let V = view() { nodes };
union withsource=tableName E, V
| evaluate python(
//
typeof(plotly:string),
pythonCodeBlueprint,
// Parameter configuration for graph visualization
// Customize these values to adapt the visualization to your data schema and preferences
bag_pack(
    // Data schema mapping - configure these based on your data structure
    'node_or_edge_column', 'tableName',    // Column that distinguishes nodes from edges (source table name)
    'node_row', 'V',                       // Value in discriminator column identifying node records
    'node_id_column', node_id_column,            // Column containing unique node identifiers
    'edge_row', 'E',                       // Value in discriminator column identifying edge records
    'source_id_column', source_id_column,        // Column containing source node IDs for edges
    'target_id_column', target_id_column,        // Column containing target node IDs for edges
    // Visualization appearance settings
    'colorscale_id', colorscale_id,            // Plotly colorscale: YlGnBu, Viridis, Plasma, Inferno, Magma, Cividis, etc.
    'diagram_title', diagram_title    // Title displayed at the top of the graph visualization
))
};
let nodes = datatable (nodeId:string, nodeType:string, importance:int)[
    'A', 'server', 5,
    'B', 'database', 3,
    'C', 'workstation', 1,
    'D', 'router', 4,
    'E', 'firewall', 5,
    'F', 'workstation', 1
];
let edges = datatable (sourceId:string, targetId:string, connectionType:string)[
    'A', 'B', 'sql',
    'A', 'D', 'network',
    'B', 'C', 'query',
    'C', 'D', 'network',
    'D', 'E', 'security',
    'E', 'F', 'network'
];
plotly_graph_fl(edges, nodes, 
                    node_id_column="nodeId", 
                    source_id_column="sourceId", 
                    target_id_column="targetId",
                    colorscale_id="Viridis",
                    diagram_title="Network Infrastructure Graph")
| render plotly

输出

包含示例数据集的图形。

示例 2:BloodHound AD 数据集可视化

此示例演示了使用 BloodHound Active Directory 数据集显示更大、更复杂的图形的可视化效果。 BloodHound AD 图包含 Active Directory 环境中的安全关系和潜在攻击路径。 有关此数据集和其他可用图形数据集的详细信息,请参阅 Graph 示例数据集和示例

重要

若要使此示例成功运行,必须先运行 函数定义 代码来存储函数。

let G = graph('BloodHound_AD');
let E = G
    | graph-to-table edges with_source_id=sourceId with_target_id=targetId;
let V = G
    | graph-to-table nodes with_node_id=nodeId;
plotly_graph_fl(E, V, node_id_column="nodeId", source_id_column="sourceId", target_id_column="targetId", diagram_title="BloodHound AD dataset")
| render plotly

包含具有安全关系和潜在攻击路径的 BloodHound Active Directory 数据集的图形。

输出是可以使用“| 呈现的 Plotly JSON 字符串”在“绘图”或 Azure 数据资源管理器仪表板磁贴中呈现。 有关创建仪表板磁贴的详细信息,请参阅 使用 Azure 数据资源管理器仪表板可视化数据

该函数返回一个单列表,其中包含 plotly 交互式图形可视化效果的 JSON 表示形式。 在支持 Plotly 可视化效果的 Kusto 环境中呈现时,此图显示为交互式网络图,如下所示:

  • 表示为彩色圆圈的节点,其大小与其连接成正比
  • 作为连接相关节点的线条绘制的边缘
  • 显示节点和边缘属性的交互式悬停工具提示
  • 基于指定色阶的可自定义配色方案
  • 自动定位节点以实现最佳可视化效果的春季强制布局

可视化效果包括节点属性(如 nodeType 示例和 importance 示例中)和悬停工具提示中的边缘属性(例如 connectionType),以便轻松浏览图形数据中的关系和属性。