Dash VTK④(地形追従メッシュ)

地形追従メッシュ

地形追従メッシュ(Terrain Following Mesh)を使うと、地形を3Dのメッシュで表示することができます。

地形追従メッシュは、水文学モデリングなどの環境科学でよく使われます。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# pip install dash_bootstrap_components

import dash
import dash_vtk
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State

import random
import json
import numpy as np
import pyvista as pv
from pyvista import examples
from vtk.util.numpy_support import vtk_to_numpy

from dash_vtk.utils import presets

random.seed(42)

def toDropOption(name):
return {"label": name, "value": name}

# Get point cloud data from PyVista
uniformGrid = examples.download_crater_topo()
subset = uniformGrid.extract_subset((500, 900, 400, 800, 0, 0), (5, 5, 1))

def updateWarp(factor=1):
terrain = subset.warp_by_scalar(factor=factor)
polydata = terrain.extract_geometry()
points = polydata.points.ravel()
polys = vtk_to_numpy(polydata.GetPolys().GetData())
elevation = polydata["scalar1of1"]
min_elevation = np.amin(elevation)
max_elevation = np.amax(elevation)
return [points, polys, elevation, [min_elevation, max_elevation]]

points, polys, elevation, color_range = updateWarp(1)

# Setup VTK rendering of PointCloud
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
server = app.server

vtk_view = dash_vtk.View(
id="vtk-view",
pickingModes=["hover"],
children=[
dash_vtk.GeometryRepresentation(
id="vtk-representation",
children=[
dash_vtk.PolyData(
id="vtk-polydata",
points=points,
polys=polys,
children=[
dash_vtk.PointData(
[
dash_vtk.DataArray(
id="vtk-array",
registration="setScalars",
name="elevation",
values=elevation,
)
]
)
],
)
],
colorMapPreset="erdc_blue2green_muted",
colorDataRange=color_range,
property={"edgeVisibility": True},
showCubeAxes=True,
cubeAxesStyle={"axisLabels": ["", "", "Altitude"]},
),
dash_vtk.GeometryRepresentation(
id="pick-rep",
actor={"visibility": False},
children=[
dash_vtk.Algorithm(
id="pick-sphere",
vtkClass="vtkSphereSource",
state={"radius": 100},
)
],
),
],
)

app.layout = dbc.Container(
fluid=True,
style={"height": "100vh"},
children=[
dbc.Row(
[
dbc.Col(
children=dcc.Slider(
id="scale-factor",
min=0.1,
max=5,
step=0.1,
value=1,
marks={0.1: "0.1", 5: "5"},
)
),
dbc.Col(
children=dcc.Dropdown(
id="dropdown-preset",
options=list(map(toDropOption, presets)),
value="erdc_rainbow_bright",
),
),
dbc.Col(
children=dcc.Checklist(
id="toggle-cube-axes",
options=[
{"label": " Show axis grid", "value": "grid"},
],
value=[],
labelStyle={"display": "inline-block"},
),
),
],
style={"height": "12%", "alignItems": "center"},
),
html.Div(
html.Div(vtk_view, style={"height": "100%", "width": "100%"}),
style={"height": "88%"},
),
html.Pre(
id="tooltip",
style={
"position": "absolute",
"bottom": "25px",
"left": "25px",
"zIndex": 1,
"color": "white",
},
),
],
)


@app.callback(
[
Output("vtk-representation", "showCubeAxes"),
Output("vtk-representation", "colorMapPreset"),
Output("vtk-representation", "colorDataRange"),
Output("vtk-polydata", "points"),
Output("vtk-polydata", "polys"),
Output("vtk-array", "values"),
Output("vtk-view", "triggerResetCamera"),
],
[
Input("dropdown-preset", "value"),
Input("scale-factor", "value"),
Input("toggle-cube-axes", "value"),
],
)
def updatePresetName(name, scale_factor, cubeAxes):
points, polys, elevation, color_range = updateWarp(scale_factor)
return [
"grid" in cubeAxes,
name,
color_range,
points,
polys,
elevation,
random.random(),
]

@app.callback(
[
Output("tooltip", "children"),
Output("pick-sphere", "state"),
Output("pick-rep", "actor"),
],
[
Input("vtk-view", "clickInfo"),
Input("vtk-view", "hoverInfo"),
],
)
def onInfo(clickData, hoverData):
info = hoverData if hoverData else clickData
if info:
if (
"representationId" in info
and info["representationId"] == "vtk-representation"
):
return (
[json.dumps(info, indent=2)],
{"center": info["worldPosition"]},
{"visibility": True},
)
return dash.no_update, dash.no_update, dash.no_update
return [""], {}, {"visibility": False}

if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

地形を3Dのメッシュで表示することができました。

各コントローラを使って表示を変えたり、地形をドラッグすることにより角度を変えて表示することが可能です。

Dash VTK③(PyVista)

PyVista

PyVistaはVisualization Toolkit (VTK) 用のヘルパー・モジュールです。

VTKとのインターフェースを提供し,データセットの迅速なプロトタイピング分析,および視覚的統合を容易にします。

このモジュールは,プレゼンテーションや研究論文の科学的プロットにも使用できます。

PyVistaのインストール

PyVistaを使うためには、下記のコマンドでインストールを行います。

[コマンド]

1
pip install pyvista

サンプル実行

PyVistaを使ったサンプルを実行してみます。

PyVistaexpmanplesから lidarデータセット をダウンロードしています。(14行目)

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import dash
import dash_vtk
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, State

import numpy as np
import pyvista as pv
from pyvista import examples

np.random.seed(42)

# Get point cloud data from PyVista
dataset = examples.download_lidar()
subset = 0.2
selection = np.random.randint(
low=0, high=dataset.n_points - 1, size=int(dataset.n_points * subset)
)
points = dataset.points[selection]
xyz = points.ravel()
elevation = points[:, -1].ravel()
min_elevation = np.amin(elevation)
max_elevation = np.amax(elevation)
print(f"Number of points: {points.shape}")
print(f"Elevation range: [{min_elevation}, {max_elevation}]")

# Setup VTK rendering of PointCloud
app = dash.Dash(__name__)
server = app.server

vtk_view = dash_vtk.View(
[
dash_vtk.PointCloudRepresentation(
xyz=xyz,
scalars=elevation,
colorDataRange=[min_elevation, max_elevation],
property={"pointSize": 2},
)
]
)

app.layout = html.Div(
style={"height": "calc(100vh - 16px)"},
children=[html.Div(vtk_view, style={"height": "100%", "width": "100%"})],
)

if __name__ == "__main__":
app.run_server(debug=True)

起動に時間がかかるので気長にお待ちください。

[ブラウザで表示]

建物と木々のような風景がプロット(表示)され、ドラッグすることによりいろいろな角度から表示することができます。

Dash VTK②(Volume Rendering)

Volume Rendering

Volume Renderingを使うと、3次元表示をいろいろと変化させることができます。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import dash
import dash_html_components as html

import dash_vtk
from dash_vtk.utils import to_volume_state

try:
# VTK 9+
from vtkmodules.vtkImagingCore import vtkRTAnalyticSource
except ImportError:
# VTK =< 8
from vtk.vtkImagingCore import vtkRTAnalyticSource

# Use VTK to get some data
data_source = vtkRTAnalyticSource()
data_source.Update() # <= Execute source to produce an output
dataset = data_source.GetOutput()

# Use helper to get a volume structure that can be passed as-is to a Volume
volume_state = to_volume_state(dataset) # No need to select field

content = dash_vtk.View([
dash_vtk.VolumeRepresentation([
# GUI to control Volume Rendering
# + Setup good default at startup
dash_vtk.VolumeController(),
# Actual volume
dash_vtk.Volume(state=volume_state),
]),
])

# Dash setup
app = dash.Dash(__name__)
server = app.server

app.layout = html.Div(
style={"width": "100%", "height": "400px"},
children=[content],
)

if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

画面左上にボリュームコントローラが表示され、このコントローラを操作することにより3次元表示をいろいろと変化させることができるようになりました。

Dash VTK①(イントロダクション)

Dash VTKとは

Dash VTKは、VTKによるビジュアライゼーションをDashフレームワークの中で実現します。

VTKVisualization Toolkitを意味しており、主に医療分野でのデータビジュアライゼーションを実現するライブラリです。

特にシミュレーションやセンサーからのデータを、3次元に表現する際によく使われています。

インストール

Dash VTKをインストールするためには、次のコマンドを実行します。

[コマンド]

1
pip install dash_vtk

サンプル

Dash VTKを使った最も簡単なサンプルコードは下記の通りです。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import dash
import dash_html_components as html

import dash_vtk
from dash_vtk.utils import to_mesh_state

try:
# VTK 9+
from vtkmodules.vtkImagingCore import vtkRTAnalyticSource
except ImportError:
# VTK =< 8
from vtk.vtkImagingCore import vtkRTAnalyticSource

# Use VTK to get some data
data_source = vtkRTAnalyticSource()
data_source.Update() # <= Execute source to produce an output
dataset = data_source.GetOutput()

# Use helper to get a mesh structure that can be passed as-is to a Mesh
# RTData is the name of the field
mesh_state = to_mesh_state(dataset)

content = dash_vtk.View([
dash_vtk.GeometryRepresentation([
dash_vtk.Mesh(state=mesh_state)
]),
])

# Dash setup
app = dash.Dash(__name__)
server = app.server

app.layout = html.Div(
style={"width": "100%", "height": "400px"},
children=[content],
)

if __name__ == "__main__":
app.run_server(debug=True)

[ブラウザで表示]

立方体が表示され、ドラッグすると角度を変えて表示することができます。

Dash Cytoscape⑳(イベントコールバック/mouseoverEdgeData)

イベントコールバック/mouseoverEdgeData

mouseoverEdgeDataを使うと、カーソルをのせたエッジのデータ辞書を取得することができます。

コールバック関数の入力(Input)にmouseoverEdgeDataを指定します。(41行目)

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import json

import dash
import dash_cytoscape as cyto
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output

app = dash.Dash(__name__)

nodes = [
{'data': {'id': short, 'label': label} }
for short, label in (
('la', 'Los Angeles'), ('nyc', 'New York'), ('to', 'Toronto'),
('mtl', 'Montreal'), ('van', 'Vancouver'), ('chi', 'Chicago'),
('bos', 'Boston'), ('hou', 'Houston')
)
]

edges = [
{'data': {'source': source, 'target': target}}
for source, target in (
('van', 'la'), ('la', 'chi'), ('hou', 'chi'),
('to', 'mtl'), ('mtl', 'bos'), ('nyc', 'bos'),
('to', 'hou'), ('to', 'nyc'), ('la', 'nyc'),
('nyc', 'bos')
)
]

app.layout = html.Div([
cyto.Cytoscape(
id='cytoscape-event',
layout={'name': 'circle'},
elements=edges+nodes,
style={'width': '100%', 'height': '450px'}
),
html.Pre(id='show_json')
])

@app.callback(Output('show_json', 'children'),
Input('cytoscape-event', 'mouseoverEdgeData'))
def displayData(data):
return json.dumps(data, indent=2)

if __name__ == '__main__':
app.run_server(debug=True)

取得したエッジの情報は、json.dumps関数を使って文字列に変換しています。(44行目)

[ブラウザで表示]

カーソルを移動してエッジに合わせると、そのエッジの情報がブラウザ下部に表示されることを確認できます。

Dash Cytoscape⑲(イベントコールバック/mouseoverNodeData)

イベントコールバック/mouseoverNodeData

mouseoverNodeDataを使うと、カーソルをのせたノードのデータ辞書を取得することができます。

コールバック関数の入力(Input)にmouseoverNodeDataを指定します。(41行目)

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import json

import dash
import dash_cytoscape as cyto
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output

app = dash.Dash(__name__)

nodes = [
{'data': {'id': short, 'label': label} }
for short, label in (
('la', 'Los Angeles'), ('nyc', 'New York'), ('to', 'Toronto'),
('mtl', 'Montreal'), ('van', 'Vancouver'), ('chi', 'Chicago'),
('bos', 'Boston'), ('hou', 'Houston')
)
]

edges = [
{'data': {'source': source, 'target': target}}
for source, target in (
('van', 'la'), ('la', 'chi'), ('hou', 'chi'),
('to', 'mtl'), ('mtl', 'bos'), ('nyc', 'bos'),
('to', 'hou'), ('to', 'nyc'), ('la', 'nyc'),
('nyc', 'bos')
)
]

app.layout = html.Div([
cyto.Cytoscape(
id='cytoscape-event',
layout={'name': 'circle'},
elements=edges+nodes,
style={'width': '100%', 'height': '450px'}
),
html.Pre(id='show_json')
])

@app.callback(Output('show_json', 'children'),
Input('cytoscape-event', 'mouseoverNodeData'))
def displayData(data):
if data:
return "You hovered over the city: " + data['label']

if __name__ == '__main__':
app.run_server(debug=True)

上記ソースでは、カーソルをのせたノードのラベルを表示しています。

[ブラウザで表示]

カーソルを移動してノードに合わせると、そのノードのラベルがブラウザ下部に表示されることを確認できます。

Dash Cytoscape⑱(イベントコールバック/tapEdgeData)

イベントコールバック/tapEdgeData

tapEdgeDataを使って、クリックしたエッジのデータ辞書を取得します。

コールバック関数の入力(Input)にtapEdgeDataを指定しています。(62行目)

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import json

import dash
import dash_cytoscape as cyto
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output

app = dash.Dash(__name__)

styles = {
'pre': {
'border': 'thin lightgrey solid',
'overflowX': 'scroll'
}
}

nodes = [
{
'data': {'id': short, 'label': label},
'position': {'x': 20*lat, 'y': -20*long}
}
for short, label, long, lat in (
('la', 'Los Angeles', 34.03, -118.25),
('nyc', 'New York', 40.71, -74),
('to', 'Toronto', 43.65, -79.38),
('mtl', 'Montreal', 45.50, -73.57),
('van', 'Vancouver', 49.28, -123.12),
('chi', 'Chicago', 41.88, -87.63),
('bos', 'Boston', 42.36, -71.06),
('hou', 'Houston', 29.76, -95.37)
)
]

edges = [
{'data': {'source': source, 'target': target}}
for source, target in (
('van', 'la'),
('la', 'chi'),
('hou', 'chi'),
('to', 'mtl'),
('mtl', 'bos'),
('nyc', 'bos'),
('to', 'hou'),
('to', 'nyc'),
('la', 'nyc'),
('nyc', 'bos')
)
]

app.layout = html.Div([
cyto.Cytoscape(
id='cytoscape-event-callbacks-1',
layout={'name': 'circle'},
elements=edges+nodes,
style={'width': '100%', 'height': '450px'}
),
html.Pre(id='cytoscape-tapEdgeData-json', style=styles['pre'])
])

@app.callback(Output('cytoscape-tapEdgeData-json', 'children'),
Input('cytoscape-event-callbacks-1', 'tapEdgeData'))
def displayTapEdgeData(data):
return json.dumps(data, indent=2)

if __name__ == '__main__':
app.run_server(debug=True)

取得したエッジ情報は、json.dumps関数を使って文字列に変換しています。(64行目)

[ブラウザで表示]

エッジをクリックするとそのエッジの情報がブラウザ下部に表示されることを確認できます。

Dash Cytoscape⑰(イベントコールバック/tapNodeData)

イベントコールバック/tapNodeData

tapNodeDataを使って、クリックしたノードのデータ辞書を取得します。

コールバック関数の入力(Input)にtapNodeDataを指定しています。(63行目)

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import json

import dash
import dash_cytoscape as cyto
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output

app = dash.Dash(__name__)

styles = {
'pre': {
'border': 'thin lightgrey solid',
'overflowX': 'scroll'
}
}

nodes = [
{
'data': {'id': short, 'label': label},
'position': {'x': 20*lat, 'y': -20*long}
}
for short, label, long, lat in (
('la', 'Los Angeles', 34.03, -118.25),
('nyc', 'New York', 40.71, -74),
('to', 'Toronto', 43.65, -79.38),
('mtl', 'Montreal', 45.50, -73.57),
('van', 'Vancouver', 49.28, -123.12),
('chi', 'Chicago', 41.88, -87.63),
('bos', 'Boston', 42.36, -71.06),
('hou', 'Houston', 29.76, -95.37)
)
]

edges = [
{'data': {'source': source, 'target': target}}
for source, target in (
('van', 'la'),
('la', 'chi'),
('hou', 'chi'),
('to', 'mtl'),
('mtl', 'bos'),
('nyc', 'bos'),
('to', 'hou'),
('to', 'nyc'),
('la', 'nyc'),
('nyc', 'bos')
)
]

app.layout = html.Div([
cyto.Cytoscape(
id='cytoscape-event-callbacks-1',
layout={'name': 'circle'},
elements=edges+nodes,
style={'width': '100%', 'height': '450px'}
),
html.Pre(id='cytoscape-tapNodeData-json', style=styles['pre'])
])


@app.callback(Output('cytoscape-tapNodeData-json', 'children'),
Input('cytoscape-event-callbacks-1', 'tapNodeData'))
def displayTapNodeData(data):
return json.dumps(data, indent=2)

if __name__ == '__main__':
app.run_server(debug=True)

取得したノード情報は、json.dumps関数を使って文字列に変換しています。(65行目)

[ブラウザで表示]

ノードをクリックするとそのノードの情報がブラウザ下部に表示されることを確認できます。

Dash Cytoscape⑯(イベントコールバック)

イベントコールバックとは

ノードやエッジなどDash Cytoscapeコンポーネントに対する操作をトリガーに、ネットワーク図やほかのコンポーネントを変化させることができます。

このようなコールバックのことをイベントコールバックと呼びます。

イベントコールバック一覧

イベントコールバックを一覧にまとめます。

イベント 内容
tapNode クリックしたノードの要素辞書全体を取得する。
tapNodeData クリックしたノードのデータ辞書を取得する。
tapEdge クリックしたエッジの要素辞書全体を取得する。
tapEgdeData クリックしたエッジのデータ辞書を取得する。
mouseoverNodeData マウスオーバーしたノードのデータ辞書を取得する。
mouseoverEdgeData マウスオーバーしたエッジのデータ辞書を取得する。
selectedNodeData 選択したノードのデータ辞書を取得する。
selectedEdgeData 選択したエッジのデータ辞書を取得する。

次回からはイベントコールバックを使ったサンプルを実行していきます。

Dash Cytoscape⑮(コールバック/ノードの追加と削除)

コールバック2

今回はコールバックを使って、ノードの追加削除を行います。

[ソースコード]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import dash
import dash_cytoscape as cyto
import dash_html_components as html
import dash_core_components as dcc
from pprint import pprint
from dash.dependencies import Input, Output, State

app = dash.Dash(__name__)

nodes = [
{
'data': {'id': short, 'label': label},
'position': {'x': 20*lat, 'y': -20*long}
}
for short, label, long, lat in (
('la', 'Los Angeles', 34.03, -118.25),
('nyc', 'New York', 40.71, -74),
('to', 'Toronto', 43.65, -79.38),
('mtl', 'Montreal', 45.50, -73.57),
('van', 'Vancouver', 49.28, -123.12),
('chi', 'Chicago', 41.88, -87.63),
('bos', 'Boston', 42.36, -71.06),
('hou', 'Houston', 29.76, -95.37)
)
]

edges = [
{'data': {'source': source, 'target': target}}
for source, target in (
('van', 'la'),
('la', 'chi'),
('hou', 'chi'),
('to', 'mtl'),
('mtl', 'bos'),
('nyc', 'bos'),
('to', 'hou'),
('to', 'nyc'),
('la', 'nyc'),
('nyc', 'bos')
)
]

default_stylesheet = [
{
'selector': 'node',
'style': {
'background-color': '#BFD7B5',
'label': 'data(label)'
}
},
{
'selector': 'edge',
'style': {
'line-color': '#A3C4BC'
}
}
]

app.layout = html.Div([
html.Div([
html.Button('Add Node', id='btn-add-node', n_clicks_timestamp=0),
html.Button('Remove Node', id='btn-remove-node', n_clicks_timestamp=0)
]),

cyto.Cytoscape(
id='cytoscape-elements-callbacks',
layout={'name': 'cose'},
stylesheet=default_stylesheet,
style={'width': '100%', 'height': '450px'},
elements=edges+nodes
)
])

@app.callback(Output('cytoscape-elements-callbacks', 'elements'),
Input('btn-add-node', 'n_clicks_timestamp'),
Input('btn-remove-node', 'n_clicks_timestamp'),
State('cytoscape-elements-callbacks', 'elements'))
def update_elements(btn_add, btn_remove, elements):
current_nodes, deleted_nodes = get_current_and_deleted_nodes(elements)
# If the add button was clicked most recently and there are nodes to add
if int(btn_add) > int(btn_remove) and len(deleted_nodes):

# We pop one node from deleted nodes and append it to nodes list.
current_nodes.append(deleted_nodes.pop())
# Get valid edges -- both source and target nodes are in the current graph
cy_edges = get_current_valid_edges(current_nodes, edges)
return cy_edges + current_nodes

# If the remove button was clicked most recently and there are nodes to remove
elif int(btn_remove) > int(btn_add) and len(current_nodes):
current_nodes.pop()
cy_edges = get_current_valid_edges(current_nodes, edges)
return cy_edges + current_nodes

# Neither have been clicked yet (or fallback condition)
return elements

def get_current_valid_edges(current_nodes, all_edges):
"""Returns edges that are present in Cytoscape:
its source and target nodes are still present in the graph.
"""
valid_edges = []
node_ids = {n['data']['id'] for n in current_nodes}

for e in all_edges:
if e['data']['source'] in node_ids and e['data']['target'] in node_ids:
valid_edges.append(e)
return valid_edges

def get_current_and_deleted_nodes(elements):
"""Returns nodes that are present in Cytoscape and the deleted nodes
"""
current_nodes = []
deleted_nodes = []

# get current graph nodes
for ele in elements:
# if the element is a node
if 'source' not in ele['data']:
current_nodes.append(ele)

# get deleted nodes
node_ids = {n['data']['id'] for n in current_nodes}
for n in nodes:
if n['data']['id'] not in node_ids:
deleted_nodes.append(n)

return current_nodes, deleted_nodes

if __name__ == '__main__':
app.run_server(debug=True)

コールバック関数(update_elements)では、現在表示されているノード(current_nodes)と削除済みのノード(deleted_nodes)を管理しています。

追加ボタンがクリックされた場合は、削除済みノードの1つを表示ノードにし、エッジ(ノードとノードを結ぶ線)も合わせて表示します。

削除ボタンがクリックされた場合は、表示ノードの1つを削除済みノードとして、エッジと合わせて非表示にします。

[ブラウザで表示]

Add Nodeボタンを押すとノードが追加され、Remove Nodeボタンを押すとノードが削除されることを確認できます。