# -*- coding: utf-8 -*-
"""
This module provides the necessary functions to start up a local
HTTP server and open an interactive d3-visualization of one or
several temporal networks.
"""
from __future__ import print_function
import os
import sys
import http.server
import webbrowser
import time
import threading
import copy
import wget
import shutil
import json
from distutils.dir_util import copy_tree, mkpath
import tacoma as tc
from tacoma.data_io import mkdirp_customdir
ht09_config = {
"temporal_network_files": [
],
"edges_coordinate_files": [
],
"plot_width": 320,
"titles": [
],
"network_plot_height": 250,
"edges_plot_height": 100,
"padding": 10,
"start_it": 0,
"node_radius": 2.5,
"link_distance": 10,
"node_charge": -8,
"edge_line_width": 1,
"font_size_in_px": 14,
"link_width": 1,
"d3_format_string": ".3f",
}
dtu_config = {
"plot_width" : 320 ,
"network_plot_height" : 250,
"edges_plot_height" : 100,
"padding" : 10,
"start_it" : 0,
"node_radius" : 1.5,
"link_distance" : 7,
"node_charge": -3,
"edge_line_width" : 0.5,
"node_edge_width" : 1,
"font_size_in_px" : 14,
"link_width" : 0.8
}
hs13_config = {
"plot_width" : 320 ,
"network_plot_height" : 350,
"edges_plot_height" : 100,
"padding" : 10,
"start_it" : 0,
"node_radius" : 2.5,
"link_distance" : 8,
"node_charge": -5,
"edge_line_width" : 1,
"font_size_in_px" : 14,
"link_width" : 1,
"node_edge_width": 1
}
standard_config = ht09_config
html_source_path = os.path.join(tc.__path__[0], 'interactive')
def _make_and_get_directory(path):
"""Simulate ``mkdir -p`` and return the path of the repository"""
directory, _ = os.path.split(
os.path.abspath(os.path.expanduser(path))
)
mkdirp_customdir(directory)
return directory
[docs]def download_d3():
"""Download `d3.v4.min.js` and save it in `~/.tacoma/d3`, if the file does not exist yet."""
url = "https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"
filename = "~/.tacoma/web/d3.v4/d3.v4.min.js"
target_name = "d3.min.js"
if not os.path.exists(os.path.abspath(os.path.expanduser(filename))):
# get directory name for download
directory = _make_and_get_directory(filename)
# download
print("downloading d3 ...")
wget.download(url, out=directory)
# move to better name
os.rename(os.path.join(directory, target_name),
os.path.abspath(os.path.expanduser(filename)))
[docs]def prepare_visualization_directory():
"""Move all files from the tacoma/interactive directory to ~/.tacoma/web"""
src = html_source_path
dst = os.path.abspath(os.path.expanduser("~/.tacoma/web/"))
#if not os.path.exists(os.path.join(dst, "index.html")):
# always copy source files to the subdirectory
copy_tree(src, dst)
[docs]def prepare_export_directory(d,subdir):
"""Move all files from the tacoma/interactive directory to directory ``d``"""
src = html_source_path
dst = os.path.abspath(os.path.expanduser(d))
# make directory and subdirectory
mkpath(os.path.join(dst, subdir))
# always copy source files to the subdirectory
copy_tree(src, dst)
d3file = os.path.abspath(os.path.expanduser("~/.tacoma/web/d3.v4/d3.v4.min.js"))
new_dst = os.path.join(dst,"d3.v4")
if not os.path.exists(d3file):
raise FileNotFoundError('D3 has not been downloaded yet. Please call `tacoma.interactive.download_d3()`.')
# copy d3 from the tacoma directory
src = os.path.abspath(os.path.expanduser("~/.tacoma/web/d3.v4/"))
copy_tree(src, new_dst)
[docs]class StoppableHTTPServer(http.server.HTTPServer):
"""Taken from https://stackoverflow.com/questions/268629/how-to-stop-basehttpserver-serve-forever-in-a-basehttprequesthandler-subclass """
def __init__(self, server_address, handler, subfolder):
http.server.HTTPServer.__init__(self, server_address, handler)
self.subfolder = subfolder
while subfolder.endswith('/'):
subfolder = subfolder[:-1]
self.subjson = subfolder + '_config.json'
[docs] def run(self):
try:
self.serve_forever()
except OSError:
pass
[docs] def stop_this(self):
# Clean-up server (close socket, etc.)
print('was asked to stop the server')
self.server_close()
# try:
if os.path.exists(self.subjson):
os.remove(self.subjson)
# except
if os.path.exists(self.subfolder):
try:
# os.rmdir(self.subfolder)
shutil.rmtree(self.subfolder)
except FileNotFoundError as e:
raise e
# os.chdir(self.cwd)
print('deleted all files')
# def __del__(self):
# self.stop_this()
def _get_prepared_network(tn, dt, time_unit, time_normalization_factor):
"""Prepare the provided network (i.e. bin it in discrete time)"""
tn_b = tc.bin(tn, dt=dt) # rebin the network
# rescale the network's time
tn_b.t = [t * time_normalization_factor for t in tn_b.t]
tn_b.tmax *= time_normalization_factor
if time_unit is None:
time_unit = ""
tn_b.time_unit = time_unit # set time unit
return tn_b
[docs]def visualize(temporal_networks,
frame_dt,
time_normalization_factor=1,
time_unit=None,
titles=None,
config=None,
port=8226,
export_path=None,
):
"""
Visualize a temporal network or a list of temporal networks interactively.
This routine starts up an HTTP server, bins the networks according to the
time step ``frame_dt`` and copies them to ``~/.tacoma/web``. Subsequently,
a the interaction is started in the standard browser.
The visualization is stopped with KeyboardInterrupt. The temporary
temporal network files will subsequently be deleted.
Parameters
----------
temporal_networks : an instance of :class:`_tacoma.edge_changes`, :class:`_tacoma.edge_lists`, :class:`_tacoma.edge_lists_with_histograms`, :class:`_tacoma.edge_changes_with_histograms` or a list containing those.
The temporal networks to visualize. If a list is provided, all networks need to have the
same `t0` and `tmax`.
frame_dt : float
The duration of a frame in the visualization.
.. note::
This has to be given in the original time units
of the temporal network, disregarding any
value of ``time_normalization_factor``.
time_normalization_factor : float, default : 1.0
Rescale time with this factor.
time_unit : string, default : None,
Unit of time of the visualization.
titles : string or list of strings, default : None
Titles to put on the figures of the corresponding temporal networks.
config : dict or str
Configuration values for the JavaScript visualization. If this
is a string, it can be either ``hs13``, ``dtu``, or ``ht09`` and
the appropriate configuration is loaded.
port : int, default : 8226
Port of the started HTTP server.
export_path : string, default : None
path to a directory to which the whole visualization is copied.
Use ``os.get_cwd()+'/export_dir/'`` for the current working directory (after
``import os``).
.. warning::
No subdirectory will be made for the export. All visualization files
will be exported to ``export_path`` directly.
Notes
-----
The configuration dictionary is filled with values to control
the appearance of the visualizations. The standard configuration is
.. code:: python
config = {
"plot_width" : 320 ,
"network_plot_height" : 250,
"edges_plot_height" : 100,
"padding" : 10,
"start_it" : 0,
"node_radius" : 2.5,
"link_distance" : 10,
"node_charge": -8,
"edge_line_width" : 1,
"font_size_in_px" : 14,
"link_width" : 1,
"d3_format_string": ".3f",
}
"""
if not hasattr(temporal_networks, '__len__'):
temporal_networks = [temporal_networks]
if titles is None:
titles = ["" for _ in temporal_networks]
elif type(titles) == str or not hasattr(titles, '__len__'):
titles = [titles]
this_config = copy.deepcopy(standard_config)
if isinstance(config, str):
if config == 'dtu':
config = dict(dtu_config)
elif config == 'hs13':
config = dict(hs13_config)
elif config == 'ht09':
config = dict(ht09_config)
else:
raise ValueError("config", config, "is unknown.")
if config is not None:
this_config.update(config)
# print(titles)
# define the server address
# server_address = ('127.0.0.1', port)
path = '~/.tacoma/web/'
web_dir = os.path.abspath(os.path.expanduser(path))
# download d3 if that did not happen yet
download_d3()
# copy the html and js files for the visualizations
prepare_visualization_directory()
# create a subfolder based on the current time
subdir = "tmp_{:x}".format(int(time.time()*1000))
mkdirp_customdir(directory=web_dir)
subdir_path = os.path.join(web_dir, subdir)
mkdirp_customdir(directory=subdir_path)
# in case an export is demanded, prepare the export directory
if export_path is not None:
export_path = os.path.abspath(os.path.expanduser(export_path))
prepare_export_directory(export_path, subdir)
# change directory to this directory
print("changing directory to", web_dir)
print("starting server here ...", web_dir)
cwd = os.getcwd()
os.chdir(web_dir)
server = StoppableHTTPServer(("127.0.0.1", port),
http.server.SimpleHTTPRequestHandler,
subdir_path,
)
for itn, tn in enumerate(temporal_networks):
print("preparing network", titles[itn])
tn_b = _get_prepared_network(
tn, frame_dt, time_unit, time_normalization_factor)
taco_fname = os.path.join(subdir, subdir+'_'+str(itn)+'.taco')
edge_fname = os.path.join(subdir, subdir+'_'+str(itn)+'.json')
tc.write_edge_trajectory_coordinates(tn_b,
os.path.join(web_dir, edge_fname),
filter_for_duration=frame_dt * time_normalization_factor)
tc.write_json_taco(tn_b, os.path.join(web_dir, taco_fname))
this_config['temporal_network_files'].append(taco_fname)
this_config['edges_coordinate_files'].append(edge_fname)
this_config['titles'].append(titles[itn])
with open(os.path.join(web_dir, subdir+'_config.json'), 'w') as f:
json.dump(this_config, f)
if export_path is not None:
copy_tree(subdir_path, os.path.join(export_path, subdir))
with open(os.path.join(export_path, 'default_config.json'), 'w') as f:
json.dump(this_config, f)
# ========= start server ============
thread = threading.Thread(None, server.run)
thread.start()
webbrowser.open("http://localhost:"+str(port)+"/?data=" + subdir)
try:
while True:
time.sleep(2)
except KeyboardInterrupt:
# thread.join()
print('stopping server ...')
server.stop_this()
thread.join()
# time.sleep(1)
print('changing directory back to', cwd)
os.chdir(cwd)
if __name__ == "__main__":
# download_d3()
a = tc.load_json_taco("~/.tacoma/ht09.taco")
visualize(a, frame_dt=20, titles='HT09', time_unit='h',
time_normalization_factor=1./3600.)