Динамическое обновление цвета узла и всплывающих подсказок с помощью ползунка — Bokeh и 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)

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

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

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
774
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

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)

Результат:

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

moe_95 10.04.2019 17:27

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

Tony 10.04.2019 17:45

Идеальный Тони, большое спасибо за вашу помощь.

moe_95 10.04.2019 17:52

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

Tony 16.04.2019 09:54

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

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.info(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)

Другие вопросы по теме