Динамическое обновление цвета узла и всплывающих подсказок с помощью slider — Bokeh и NetworkX

#bokeh #networkx

#боке #networkx

Вопрос:

У меня есть слайдер, который будет ограничивать количество узлов, отображаемых на графике. Однако, когда используется слайдер, всплывающие подсказки для проверки атрибутов узла перестают функционировать и ??? вместо этого отображаются.

Кроме того, при использовании слайдера раскраска узлов работает некорректно. При проверке консоли в Chrome отображается следующая ошибка: Uncaught Error: attempted to retrieve property array for nonexistent field 'node_colors' . Я полагаю, что это связано с несоответствием длины массива, переданного в node_renderer.glyph код. Текущая раскраска окрашивает все source значения в зеленый цвет, а target значения — в синий.

Полный код для описанного решения можно увидеть ниже:

 import networkx as nx
from bokeh.io import show, output_file
from bokeh.models import Plot, Range1d, MultiLine, Circle, TapTool, OpenURL, HoverTool, CustomJS, Slider, Column
from bokeh.models.graphs import from_networkx, EdgesAndLinkedNodes
from bokeh.palettes import Spectral4
from dask.dataframe.core import DataFrame
import pandas as pd

data = {'source': ['A', 'A', 'A', 'B', 'B', 'B'], 'target': ['C', 'D', 'E', 'F', 'G', 'H'], 'source_count': [15, 15, 15, 25, 25, 25], 'target_count': [10, 20, 30, 10, 20, 30]}
df = pd.DataFrame(data)

net_graph = nx.from_pandas_edgelist(df, 'source', 'target')

for index, row in df.iterrows():
    net_graph.nodes[row['source']]['yearly_count'] = row['source_count']
    net_graph.nodes[row['target']]['yearly_count'] = row['target_count']

node_colors = []
for node in net_graph:
    if node in df["source"].values:
        node_colors.append("green")
    else: node_colors.append("maroon")

graph_plot = Plot(plot_width = 800, plot_height = 600, x_range = Range1d(-1.1, 1.1), y_range = Range1d(-1.1, 1.1))

node_hover_tool = HoverTool(tooltips = [("Name", "@index"), ("Yearly Count", "@yearly_count")])
graph_plot.add_tools(node_hover_tool)

graph_setup = from_networkx(net_graph, nx.spring_layout, scale = 1, center = (0, 0))

graph_setup.node_renderer.data_source.data['node_colors'] = node_colors
graph_setup.node_renderer.glyph = Circle(size = 20, fill_color = 'node_colors')
graph_setup.edge_renderer.glyph = MultiLine(line_color = "red", line_alpha = 0.8, line_width = 1)

graph_plot.renderers.append(graph_setup)

code = """ 
    var new_start = start.slice();
    var new_end = end.slice();
    new_index = end.slice();

    new_start = new_start.splice(0, cb_obj.value)
    new_end = new_end.splice(0, cb_obj.value)
    new_index = ['A','B'].concat(new_end)

    new_data_edge = {'start': new_start, 'end': new_end};
    new_data_nodes = {'index': new_index};
    graph_setup.edge_renderer.data_source.data = new_data_edge; 
    graph_setup.node_renderer.data_source.data = new_data_nodes; 
"""
callback = CustomJS(args = dict(graph_setup = graph_setup,
                                start = df['source'].values,
                                end = df['target'].values), code = code)

slider = Slider(title = 'Slider', start = 0, end = 6, value = 6)
slider.js_on_change('value', callback)

layout = Column(graph_plot, slider)
show(layout)
  

Данные, включенные в пример кода, представляют собой фрагмент общего фрейма данных.

Любая помощь, которую кто-либо может предоставить, будет высоко оценена.

Ответ №1:

Пожалуйста, замените свой код обратного вызова на этот:

 code = """ 
    var new_start = start.slice();
    var new_end = end.slice();

    var new_index = ndata['index'].slice();
    var new_node_colors = ndata['node_colors'].slice();
    var new_yearly_count = ndata['yearly_count'].slice();

    new_start = new_start.splice(0, cb_obj.value)
    new_end = new_end.splice(0, cb_obj.value)

    new_data_edge = {'start': new_start, 'end': new_end};

    new_data_nodes = {};    
    new_data_nodes['index'] = new_index.splice(0, cb_obj.value);
    new_data_nodes['node_colors'] = new_node_colors.splice(0, cb_obj.value);
    new_data_nodes['yearly_count'] = new_yearly_count.splice(0, cb_obj.value);

    graph_setup.edge_renderer.data_source.data = new_data_edge; 
    graph_setup.node_renderer.data_source.data = new_data_nodes;    
"""
  

И добавьте это в свой код Python:

 import copy

backup_node_data = copy.deepcopy(graph_setup.node_renderer.data_source.data)

callback = CustomJS(args = dict(graph_setup = graph_setup,
                                start = df['source'].values,
                                end = df['target'].values,
                                ndata = backup_node_data),
                    code = code)
  

Или замените весь ваш код на этот:

 import networkx as nx
from bokeh.io import show, output_file
from bokeh.models import Plot, Range1d, MultiLine, Circle, TapTool, OpenURL, HoverTool, CustomJS, Slider, Column
from bokeh.models.graphs import from_networkx, EdgesAndLinkedNodes
from bokeh.palettes import Spectral4
from dask.dataframe.core import DataFrame
import pandas as pd
import copy

data = {'source': ['A', 'A', 'A', 'B', 'B', 'B'], 'target': ['C', 'D', 'E', 'F', 'G', 'H'], 'source_count': [15, 15, 15, 25, 25, 25], 'target_count': [10, 20, 30, 10, 20, 30]}
df = pd.DataFrame(data)

net_graph = nx.from_pandas_edgelist(df, 'source', 'target')

for index, row in df.iterrows():
    net_graph.nodes[row['source']]['yearly_count'] = row['source_count']
    net_graph.nodes[row['target']]['yearly_count'] = row['target_count']

node_colors = []
for node in net_graph:
    if node in df["source"].values:
        node_colors.append("green")
    else:
        node_colors.append("maroon")

graph_plot = Plot(plot_width = 800, plot_height = 600, x_range = Range1d(-1.1, 1.1), y_range = Range1d(-1.1, 1.1))

node_hover_tool = HoverTool(tooltips = [("Name", "@index"), ("Yearly Count", "@yearly_count")], show_arrow = False)
graph_plot.add_tools(node_hover_tool)

graph_setup = from_networkx(net_graph, nx.spring_layout, scale = 1, center = (0, 0))

graph_setup.node_renderer.data_source.data['node_colors'] = node_colors
graph_setup.node_renderer.glyph = Circle(size = 20, fill_color = 'node_colors')
graph_setup.edge_renderer.glyph = MultiLine(line_color = "red", line_alpha = 0.8, line_width = 1)

graph_plot.renderers.append(graph_setup)

backup_node_data = copy.deepcopy(graph_setup.node_renderer.data_source.data)

    code = """ 
        var new_start = start.slice();
        var new_end = end.slice();

        var new_index = ndata['index'].slice();
        var new_node_colors = ndata['node_colors'].slice();
        var new_yearly_count = ndata['yearly_count'].slice();

        new_start = new_start.splice(0, cb_obj.value)
        new_end = new_end.splice(0, cb_obj.value)

        new_data_edge = {'start': new_start, 'end': new_end};

        new_data_nodes = {};    
        new_data_nodes['index'] = new_index.splice(0, cb_obj.value);
        new_data_nodes['node_colors'] = new_node_colors.splice(0, cb_obj.value);
        new_data_nodes['yearly_count'] = new_yearly_count.splice(0, cb_obj.value);

        graph_setup.edge_renderer.data_source.data = new_data_edge; 
        graph_setup.node_renderer.data_source.data = new_data_nodes;    
    """
    callback = CustomJS(args = dict(graph_setup = graph_setup,
                                    start = df['source'].values,
                                    end = df['target'].values,
                                    ndata = backup_node_data),
                        code = code)

    slider = Slider(title = 'Slider', start = 0, end = 8, value = 8)
    slider.js_on_change('value', callback)

    layout = Column(graph_plot, slider)
    show(layout)
  

Результат:

введите описание изображения здесь

Комментарии:

1. Это гениально, Тони, большое спасибо! Знаете ли вы, как заставить ползунок удалять только target узлы? Т.Е. Все исходные узлы будут видны, когда ползунок установлен в 0 положение. Еще раз спасибо!

2. Это сложнее. Вам нужно будет разделить source на A и B . Я изучу это и вернусь к этому позже или завтра.

3. Отлично, Тони, большое спасибо за всю вашу помощь.

4. Извините, я забыл вас. Я выполнил часть скрытия / отображения узлов, но я не делал ребер, поэтому всегда остается два ребра. Вам все еще нужен этот код?

Ответ №2:

Другая версия, которая всегда оставляет центральные узлы:

 import networkx as nx
from bokeh.io import show, output_file
from bokeh.models import Plot, Range1d, MultiLine, Circle, TapTool, OpenURL, HoverTool, CustomJS, Slider, Column
from bokeh.models.graphs import from_networkx, EdgesAndLinkedNodes
from bokeh.palettes import Spectral4
from dask.dataframe.core import DataFrame
import pandas as pd
import copy

data = {'source': ['A', 'A', 'A', 'B', 'B', 'B'], 'target': ['C', 'D', 'E', 'F', 'G', 'H'], 'source_count': [15, 15, 15, 25, 25, 25], 'target_count': [10, 20, 30, 10, 20, 30]}
df = pd.DataFrame(data)

net_graph = nx.from_pandas_edgelist(df, 'source', 'target')

for index, row in df.iterrows():
    net_graph.nodes[row['source']]['yearly_count'] = row['source_count']
    net_graph.nodes[row['target']]['yearly_count'] = row['target_count']

node_colors = []
for node in net_graph:
    if node in df["source"].values:
        node_colors.append("green")
    else:
        node_colors.append("maroon")

graph_plot = Plot(plot_width = 800, plot_height = 600, x_range = Range1d(-1.1, 1.1), y_range = Range1d(-1.1, 1.1))

node_hover_tool = HoverTool(tooltips = [("Name", "@index"), ("Yearly Count", "@yearly_count")])
graph_plot.add_tools(node_hover_tool)

graph_setup = from_networkx(net_graph, nx.spring_layout, scale = 1, center = (0, 0))

graph_setup.node_renderer.data_source.data['node_colors'] = node_colors
graph_setup.node_renderer.glyph = Circle(size = 20, fill_color = 'node_colors')
graph_setup.edge_renderer.glyph = MultiLine(line_color = "red", line_alpha = 0.8, line_width = 1)

graph_plot.renderers.append(graph_setup)

a_index = graph_setup.node_renderer.data_source.data['index'].index("A")
b_index = graph_setup.node_renderer.data_source.data['index'].index("B")

if a_index != 0:
        index_item = graph_setup.node_renderer.data_source.data[field][a_index]
        new_data = graph_setup.node_renderer.data_source.data[field][0:a_index]   graph_setup.node_renderer.data_source.data[field][a_index   1:]
        new_data.insert(0, index_item)
        graph_setup.node_renderer.data_source.data[field] = new_data

if b_index != 1:
    for field in graph_setup.node_renderer.data_source.data:
        index_item = graph_setup.node_renderer.data_source.data[field][b_index]
        new_data = graph_setup.node_renderer.data_source.data[field][0:b_index]   graph_setup.node_renderer.data_source.data[field][b_index   1:]
        new_data.insert(1, index_item)
        graph_setup.node_renderer.data_source.data[field] = new_data

backup_node_data = copy.deepcopy(graph_setup.node_renderer.data_source.data)
backup_edge_data = copy.deepcopy(graph_setup.edge_renderer.data_source.data)

code = """ 
    var new_start = start.slice();
    var new_end = end.slice();

    var new_index = ndata['index'].slice();
    var new_node_colors = ndata['node_colors'].slice();
    var new_yearly_count = ndata['yearly_count'].slice();

    new_start = new_start.splice(0, cb_obj.value)
    new_end = new_end.splice(0, cb_obj.value)

    new_data_edge = {'start': new_start, 'end': new_end};

    new_data_nodes = {};    
    new_data_nodes['index'] = new_index.splice(0, cb_obj.value);
    new_data_nodes['node_colors'] = new_node_colors.splice(0, cb_obj.value);
    new_data_nodes['yearly_count'] = new_yearly_count.splice(0, cb_obj.value);

    console.log(new_data_edge)
    graph_setup.edge_renderer.data_source.data = new_data_edge; 
    graph_setup.node_renderer.data_source.data = new_data_nodes;  

    graph_setup.edge_renderer.data_source.change.emit();
    graph_setup.node_renderer.data_source.change.emit();
"""
callback = CustomJS(args = dict(graph_setup = graph_setup,
                                start = df['source'].values,
                                end = df['target'].values,
                                ndata = backup_node_data,
                                edata = backup_edge_data),
                    code = code)

slider = Slider(title = 'Slider', start = 2, end = 8, value = 8)
slider.js_on_change('value', callback)

layout = Column(graph_plot, slider)
show(layout)