目标:
3年前,我已经实现了如上图所示的可视化。不幸的是,我做了太多的清洁工作,我才意识到,我不再有这些方法了。它在某种程度上是球体表面上的力导向图。可能类似于二维力图集中的“强重力”参数。我还没有找到任何 3D 实现。
我再次尝试了以下算法,但没有一个产生这种布局,也没有对这些算法进行参数调整(或者我错过了一个重要的算法?):
研究“球形图布局”并没有给我带来任何进展(仅对这个看起来非常相似的视图https://observablehq.com/@fil/3d-graph-on-sphere)。
如何使用Python(或提供定位信息的第三方)实现这种球形布局
更新:我取得了一些进展,找到了关键字非欧几里德、双曲和球面力导向算法,但仍然没有取得任何成果。或非欧几里德黎曼嵌入 (https://www2.cs.arizona.edu/~kobourov/riemann_embedders.pdf)
您尝试过 GSPBOX 的 python lib 版本吗? 如果是,为什么它对你不起作用?
https://pygsp.readthedocs.io/en/stable/reference/graphs.html
我所做的是使用networkx进行绘图,然后将其生成一个KML文件,然后将其加载到google Earth中。 人们可以轻松地将其生成到其他 GIS 工具中。
因为我使用了网络的一些预设值,所以我发现单独运行脚本并存储中间 json 对象更容易。
这是 nx.kamada_kawai_layout 正在完成的工作
import json
import networkx as nx
if __name__ == '__main__':
print(f'loading net')
# cf. networkx for the format. but here is what I am mainly doing.
# {"directed": false, "multigraph": false, "graph": {},
# "nodes": [{"id": 0}, ...],
# "links": [{"source": 0, "target": 1}, ...]}
net_f = open(f'graph.json', 'r') # load the graph definition.
net = json.load(net_f)
net_f.close()
print(f'setting network')
graph = nx.node_link_graph(net, edges="links")
print(f'loading initial positions')
# These points are normalised on the unit sphere.
# {"0": [-1.0, 0.0, 0.0], "1": [-0.866, -0.4999, 0.0] .. etc}
pos_f = open(f'pos.json', 'r')
pos_n = json.load(pos_f)
pos_f.close()
print(f'setting initial positions')
pos = {int(k): v for k, v in pos_n.items()}
print(f'invoking kamada_kawai')
k_pos = nx.kamada_kawai_layout(graph, pos=pos, dim=3)
print(f'storing solved positions')
f = open(f'solved_pos.json', 'w')
# {"0": [-0.903, -0.119, 5.309], ... etc }
f.write(json.dumps({k: tuple(v) for k, v in k_pos.items()}))
f.close()
print(f'finished')
然后要转换为 KMZ,我将需要原始计算中的多边形信息 - 这仅用于显示目的,并且仅仅是每个周期中点的索引。我想,可以添加线条等。
还需要将单位球面转换为纬度和经度。 有很多 GIS 功能可以实现这一点,或者如下所示..
为了转换为kml,我使用simpleKML。 KMZ 需要一些奇怪的调整来环绕经度边界。因此多边形加法作为函数进行管理。
import json
import simplekml
import numpy
def xyz_ll(cls, xyz: (tuple | list)) -> tuple:
x, y, z = xyz
return float(np.degrees(np.arctan2(z, np.sqrt(x * x + y * y)))), float(np.degrees(np.arctan2(y, x)))
def add_poly(kz, pts, idx, name=None, col=(154, 154, 154), alpha=0.5, width=4):
tpt = [pts[i] for i in idx]
lo = [i[1] for i in tpt]
mim = max(lo) - min(lo)
if mim > 179.9999:
path = [(o if o >= 0 else o + 360.0000, a) for (a, o) in tpt]
else:
path = [(o, a) for (a, o) in tpt]
pol = kz.newpolygon(name=name)
pol.outerboundaryis = path
pol.style.polystyle.fill = 0
pol.style.polystyle.outline = 1
pol.style.linestyle.width = width
if col:
is_i = all([(i.is_integer() or i > 1) for i in col])
(r, g, b) = [int(i) if is_i else int((i*255)) for i in col]
a = int(255 * alpha)
pol.style.polystyle.color = simplekml.Color.rgb(r, g, b, a)
if __name__ == '__main__':
pos_f = open(f'solved_pos.json', 'r')
pos_n = json.load(pos_f)
pos_f.close()
pos = dict()
for k, v in pos_n.items():
key = int(k)
pos[key] = tuple(sp.xyz_ll(tuple(v)))
# polygons are just a list of lists..
# each polygon has a name, and the point ids..
# eg [["04a", [13, 25, 28, 16, 13]], ...]
pol_f = open(f'poly.json', 'r')
polys = json.load(pol_f)
pol_f.close()
print(f'generate kml {depth}')
kml = simplekml.Kml(open=1)
for poly in polys:
add_poly(kml, pos, poly[1], poly[0])
kml.save(f'graph.kml')
print(f'kml file generated')
正在将文件加载到谷歌地球...