# Notebook from Miro-Astore/dynamic_network_analysis_scripts
Path: Step2B_Interactive_Graph.ipynb

# Interactive graph visualisation via Bokeh
https://docs.bokeh.org/en/latest/index.html_____no_output_____

# Load the python package
import os
from dynetan.toolkit import *
from dynetan.viz import *
from dynetan.proctraj import *
from dynetan.gencor import *# Load the python package
import os
from dynetan.toolkit import *
from dynetan.viz import *
from dynetan.proctraj import *
from dynetan.gencor import *
from dynetan.contact import *
from dynetan.datastorage import *

from MDAnalysis.analysis import distances as MDAdistances
#from numpy.linalg import norm
#from itertools import islice
from itertools import combinations
from scipy import stats
from collections import OrderedDict

import networkx as nx
import numpy as np
import scipy as sp
from dynetan.contact import *
from dynetan.datastorage import *

from MDAnalysis.analysis import distances as MDAdistances
#from numpy.linalg import norm
#from itertools import islice
from itertools import combinations
from scipy import stats
from collections import OrderedDict

import networkx as nx
import numpy as np
import scipy as sp_____no_output_____# For visualization
from bokeh.io import output_file, output_notebook, push_notebook, show
from bokeh import models as bokehModels
from bokeh import transform as bokehTransform
from bokeh import layouts as bokehLayouts
from bokeh import plotting as bokehPlotting
from bokeh import palettes as bokehPalettes
from bokeh import events as bokehEvents
# For pre-calculating CArtesian distances based on 2D embedding
from sklearn.manifold import MDS_____no_output_____mapResidueNames={'ALA':'A','CYS':'C','ASP':'D','GLU':'E','PHE':'F',
                 'GLY':'G','HIS':'H','HSD':'H','HSE':'H','ILE':'I','LYS':'K','LEU':'L',
                 'MET':'M','ASN':'N','PRO':'P','GLN':'Q','ARG':'R',
                 'SER':'S','THR':'T','VAL':'V','TRP':'W','TYR':'Y',
                 'MG':'Mg','POPC':'Popc',
                 'ATP':'Atp','GTP':'Gtp',
                 'NA':'Sod','SOD':'Sod','CLA':'Cl','CL':'Cl','POT':'Pot','K':'Pot',
                 'SOL':'h2o','HOH':'h2o','WAT':'h2o','TIP':'h2o','H2O':'h2o',
                }
def name_node(dnad, node):
    #i=dnad.nodesAtmSel[node].index
    resname=dnad.nodesAtmSel[node].resname ; resid=dnad.nodesAtmSel[node].resid
    return "%s%s" % (mapResidueNames[resname], resid)

def clarify_duplicate_nodes(dictNames, dictSuffix):
    """
    From two dicts with the same keys, add the respective suffix to all keys in the former that possess duplicate values.
# For visualization
from bokeh.io import output_file, output_notebook, push_notebook, show
from bokeh import models as bokehModels
from bokeh import transform as bokehTransform
from bokeh import layouts as bokehLayouts
from bokeh import plotting as bokehPlotting
from bokeh import palettes as bokehPalettes
from bokeh import events as bokehEvents
# For pre-calculating CArtesian distances based on 2D embedding
from sklearn.manifold import MDS    """
    from itertools import chain
    dictRev = {}
    for k, v in dictNames.items():
        dictRev.setdefault(v, set()).add(k)
        setDuplicateKeys = set(chain.from_iterable( v for k, v in dictRev.items() if len(v) > 1))
    for k in setDuplicateKeys:
        dictNames[k] = dictNames[k]+"_"+dictSuffix[k]
    return dictNames  _____no_output_____def encode_neighbour_information(G):
    # = = Do over each node. First enccode javascript ID, assuming that order is preserved.
    for i, x in enumerate(G.nodes()):
        G.nodes[x]['jsID']=i
    for i, x in enumerate(G.edges()):
        G.edges[x]['jsID']=i

    for n in G.nodes():
        G.nodes[n]['jsNeighbours'] = [ G.nodes[y]['jsID'] for y in G.neighbors(n) ]_____no_output_____def xy_to_th(v1,v0):
    return np.arctan2(v1[...,1]-v0[1],v1[...,0]-v0[0])

def encode_node_neighbours_with_spatial_ranking(G, posNodes, fieldID='jsID', fieldOutput='jsNeighOrdered'):
    """
    The default order of a node's neighbours is according the native sequence in the graph.
    This is often not ideal when you need to draw a subgraph from scratch.
    We can borrow from an already optimised set of 2D graph position to re-order neighbours.
    """
    for n, d in G.nodes(data=fieldID):
        if fieldID is not None:
            idNeigh = [ G.nodes[y][fieldID] for y in G.neighbors(n) ]
        else:
            idNeigh = [ y for y in G.neighbors(n) ]
        posNeigh = np.array([ posNodes[y] for y in G.neighbors(n) ])
        if len(posNeigh) > 0:
            rthNeigh = xy_to_th(posNeigh, posNodes[n])
            G.nodes[n][fieldOutput] = [x for _, x in sorted(zip(rthNeigh, idNeigh), key=lambda pair: pair[0])]
        else:
            G.nodes[n][fieldOutput] = []
        _____no_output_____def extract_node_data(dnad, nodeAttr):
    out=np.zeros( (dnad.numWinds,dnad.numNodes) )
    for w in range(dnad.numWinds):
        for x in dnad.nxGraphs[w].nodes():
            out[w,x] = dnad.nxGraphs[w].nodes[x][nodeAttr]
    return out_____no_output_____def get_maximum_edges_realspace(n):
    """
    Note: equals an icosahedron for N=12, octahedron for N=6, and tetrahedron for N=3.
    """
    if n>2:
        return 3.0*(n-2)
    elif n<2:
        return 0
    else:
        return 1_____no_output_____def get_weighted_clustering_coefficient(G, weight='weight', bRealSpace=False):
    """
    In the context of DyNetAn, this is the generalized-correlation coefficient weighted clustering coefficient.
    Also known as transitivity in other contexts, where CC is the special case for triangles or 3-cycles.

    For theoretical gaphs, the maximum possible edges between neighours is d(d-1)/2, where d is the node degree.

    Modifications need to be made for real space graphs, as it's not possible for every neighbour to
    have an edge to all other neighbours. This reduces the maximum number of possible edges in the equation.
    The 2D analogy is that edges cannot intersect in a planar graph.
    The 3D version is that edges cannot intrude into an existing volume defined by a tetrad clique,
    i.e. each additional neighbour effectively creates new tetrads or sub-dividing existing tetrads.
    N_edges = 3*(d-2) {d>2}.
    This only affects nodes with degrees > 4.
    """
    c = nx.clustering(G, weight=weight)
    if bRealSpace:
        for n, d in G.nodes(data='degree'):
            if d>4:
                c[n]=c[n]*d*(d-1)*0.5/get_maximum_edges_realspace(d)
    return c_____no_output_____def get_weighted_degree(G):
    out = {}
    for n,d in G.nodes(data='degree'):
        out[n]=np.sum( [ G.edges[n,b]['weight'] for b in G.neighbors(n) ] )
    return out_____no_output_____def get_local_cohesion(G):
    """
    Local cohesion: as defined by local clustering coefficient * node degree,
    with all edges weighted according to GCC.
    """
    if not 'cwcc' in G.nodes()[0].keys():
        cwcc = get_weighted_clustering_coefficient(G, weight='weight', bRealSpace=True)
    else:
        cwcc = nx.get_node_attributes(G, 'cwcc')
    if not 'wdeg' in G.nodes()[0].keys():
        wd = get_weighted_degree(G)
    else:
        wd = nx.get_node_attributes(G, 'wdeg')

    out  = {}
    for n,d in G.nodes(data='degree'):
        out[n]=cwcc[n]*wd[n]
    return out_____no_output_____def get_information_centrality(G, weight='weight'):
    """
    This is a more general form of betweenness centrality by considering
    all paths rather than just shortest paths.
    It uses weight as a 'conductivity' or inverse resistance.
    Also known as current-flow closeness centrality.
    """
    if nx.is_connected(G):
        return nx.information_centrality(G, weight=weight)
    else:
        print("= = = WARNING: Graph is not-fully connected. Will compute information centrality for larges sub-graph only.")
        largest_cc = max(nx.connected_components(G), key=len)
        H = G.subgraph(largest_cc)
        temp = nx.information_centrality(H, weight='weight')
        out = {}
        for n in G.nodes():
            if n in H:
                out[n] = temp[n]
            else:
                out[n] = 0.0
        return out_____no_output_____def export_graph_data(dnad, listAttr, prefix="./output"):
    import string
    for nodeAttr in listAttr:
        fileNameExport="%s_%s.dat" % (prefix, nodeAttr)
        dataBlock = extract_node_data(dnad, nodeAttr)
        dataMean  = np.mean(dataBlock,axis=0)
        dataSig   = np.std(dataBlock,axis=0)
        with open(fileNameExport, 'w') as fp:
            for i in range(dataBlock.shape[1]):
                #xVal=int(G.nodes[i]['name'].strip(string.ascii_lowercase+string.ascii_uppercase))
                xVal=G.nodes[i]['jsID']
                print("%i %f %f" % (xVal, dataMean[i], dataSig[i]), file=fp)
        fp.close()_____no_output_____

## Files and system definitions_____no_output_____

bPythonExport = False_____no_output_____if bPythonExport:
    import argparse
    parser = argparse.ArgumentParser(description='Analyse Dynamic Network Analysis graphs and generate an HTML file.',
                                                 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-i', '--input', type=str, dest='inputPrefix', default=None, required=True,
                        help='Input prefix used to generate files from Step 1.')
    parser.add_argument('-o', '--output', type=str, dest='outputHTML', default=None, required=True,
                        help='The name of the output HTML file to be generated. (.html will be appedned automatically.)')
    parser.add_argument('-r','--ref', type=str, dest='referencePrefix', default=None,
                        help='An equivalent input prefix to include a reference Step 1 computation for comparison.')
    parser.add_argument('--title', type=str, dest='titleGraph', default='Dynamic Network Analysis',
                        help='Name of the graph titles to be shown in the output HTML.')
    parser.add_argument('--import_positions', type=str, dest='fileNodePositions', default=None,
                        help='Import node positions from another invocation of this screipt to make cross-comparisons easier.')
    parser.add_argument('--export_positions', type=str, dest='fileNodeExport', default=None,
                        help='Export node positions from this invocation to make cross-comparisons easier.')
    parser.add_argument('--water_cluster_definitions', type=str, dest='fileClusterDefinitions', default=None,
                        help='Definitions of solvent clusters. If given, will be used to overwrite the existing node positions otherwise defined.')

    args = parser.parse_args()

    fullPathRoot   = args.inputPrefix
    outputFileName = args.outputHTML+".html"
    titleGraph     = args.titleGraph
    fileImportPos  = args.fileNodePositions
    fileExportPos  = args.fileNodeExport
    fileClusterDefinitions = args.fileClusterDefinitions
    bComparison = False
    if args.referencePrefix is not None:
        fullPathRootB = args.referencePrefix
        bComparison = True_____no_output_____# = = = Change to the relevant work folder
if not bPythonExport:
    systemExample="CFTR"
    if systemExample == "UbqCHARMM":
        %cd /home/zharmad/host/projects/Ubq-md
    elif systemExample == "UbqAthi":
        %cd /home/zharmad/host/shared-colleague/Ubq-2017
    elif systemExample == "periplasmic":
        %cd /home/zharmad/projects/periplasmic/leucine-binding_protein
    elif systemExample == "CFTR":
        %cd /home/zharmad/projects/cftr/DyNetAn
    else:
        %cd .._____no_output_____if not bPythonExport:
    outputDir=None
    fileImportPos=None
    fileClusterDefinitions = None
    if systemExample == "UbqCHARMM":
        # apo1 apo2 apo3
        state="apo2" ; dataDir = "./dynetan"
        fileNameRoot = "%s_5x1000" % state
        stateB="apo1" ; dataDirB = dataDir
        fileNameRootB = "%s_5x1000" % stateB
    elif systemExample == "UbqAthi":
        # UbqI13V  UbqI23A  UbqI30A  UbqL43A  UbqL67A  UbqL69A  UbqV17A  UbqWT
        state="UbqWT" ; dataDir = "./dynetan"
        fileNameRoot = "%s_5x200" % state
        stateB="UbqWT" ; dataDirB = dataDir
        fileNameRootB = "%s_5x200" % stateB
    elif systemExample == "periplasmic":
        # apo holo
        state="apo" ; dataDir = "./dynetan"
        fileNameRoot = "%s_5x200" % state
        stateB="apo" ; dataDirB = dataDir
        fileNameRootB = "%s_5x200" % stateB
    elif systemExample == "CFTR":
        # Define mutant file IO locations. wt, P67L, E56K, R75Q, S945L, dF508, Q1291H, etc.
        state="dF508" ; temperature="310K"
        dataDir = "./results/%s/%s/" % (state, temperature)
        #Path where results will be written (you may want plots and data files in a new location)
        outputDir = "./results/%s/%s/analysis" % (state, temperature)
        fileNameRoot = "1to3"
        stateB="wt" ; dataDirB = "./results/%s/%s/" % (stateB, temperature)
        fileNameRootB = "1to6"

        fileImportPos  = './CFTRGraphReferencePositions.txt'
        fileClusterDefinitions = './Stable_Solvent_Clustering.cluster_definitions_d3.5_r0.50.txt'
    else:
        Insert_Interrupt()
    #Path where results will be written (you may want plots and data files in a new location)
    if outputDir is None:
        outputDir = "./dynetan/analysis"
    fullPathRoot = os.path.join(dataDir, fileNameRoot)
    filePDBRef = fullPathRoot+"_reducedTraj.pdb"

    # outputDir is not currently used.
    outputFileName = "./networkView-%s.html" % state
    titleGraph     = "%s network" % state

    if fileImportPos is None:
        fileImportPos  = os.path.join(dataDir,'./nodeReferencePositions.txt')
    fileExportPos  = None
    if not os.path.exists( fileImportPos ):
        fileImportPos  = None
        fileExportPos  = os.path.join(dataDir,'./nodeReferencePositions.txt')

    bComparison = True
    if bComparison:
        fullPathRootB = os.path.join(dataDirB, fileNameRootB)
        filePDBRefB = fullPathRootB+"_reducedTraj.pdb"
        outputFileName = "./networkView-%s-comparison.html" % state_____no_output_____

### Load the relevant allele data into a DNAD object_____no_output_____

print("= = = Loading input graph data...")
dnad = DNAdata()
# = = = loadFromFile will automatically output debug lines.
dnad.loadFromFile(fullPathRoot)
#mdU = mda.Universe( "./UbqWT/reference.pdb" )
mdU = mda.Universe( filePDBRef )
dnad.nodesAtmSel = mdU.atoms[ dnad.nodesIxArray ]_____no_output_____

### Load Reference wildtype data if required._____no_output_____

if bComparison:
    print("= = = Loading reference data...")
    dnadB = DNAdata()
    dnadB.loadFromFile(fullPathRootB)
    mdUB = mda.Universe( filePDBRefB )
    dnadB.nodesAtmSel = mdUB.atoms[ dnadB.nodesIxArray ]
else:
    dnadB = dnad
    mdUB = mdU_____no_output_____

### Encode Additional Data into nxGraph (most of which should now be done in Step 1 and saved into the graph by default).
1. Clustering Coefficient
2. Node and Edge Betweenness Centrality
3. Node names and SegIDs
4. Edge IDs for each node._____no_output_____

def encode_additional_graph_data(dnad):
    for w in range(dnad.numWinds):
        G = dnad.nxGraphs[w]
        testNode=0
        #G.nodes()[0]
        testEdge=(0,1)
        # Note: btws is the same as the results dervied from nx.edge_betweenness_centrality()
        if not 'btws' in G.edges[testEdge].keys():
            print("   ...encoding edge betweenness into the graph.")
            for u,v in dnad.btws[w]:
                G.edges[u,v]['btws']=dnad.btws[w][u,v]

        # Note: edge-betweeness weighted clustering coefficient
        if not 'bwcc' in G.nodes[testNode].keys():
            print("   ...analysing edge-betweenness weighted clustering coefficient and encoding into graph.")
            nx.set_node_attributes(G, get_weighted_clustering_coefficient(G, weight='btws', bRealSpace=True), 'bwcc')

        # Note: GCC weighted clustering coefficient
        if not 'cwcc' in G.nodes[testNode].keys():
            print("   ...analysing GCC-weighted clustering coefficient and encoding into graph.")
            nx.set_node_attributes(G, get_weighted_clustering_coefficient(G, bRealSpace=True), 'cwcc')

        if not 'wdeg' in G.nodes[testNode].keys():
            print("   ...analysing weighted degrees and encoding into graph.")
            nx.set_node_attributes(G, get_weighted_degree(G), 'wdeg')

        if not 'cohesion' in G.nodes[testNode].keys():
            print("   ...analysing local cohesion and encoding into graph.")
            nx.set_node_attributes(G, get_local_cohesion(G), 'cohesion')

        # Note: this is the node betweennes centrality, which might be useful in a different context.
        if not 'btws' in G.nodes[testNode].keys():
            print("   ...analysing node-betweenness centrality and encoding into graph.")
            nx.set_node_attributes(G, nx.betweenness_centrality(G), 'btws')

        if not 'infc' in G.nodes[testNode].keys():
            print("   ...analysing information centrality and encoding into graph.")
            nx.set_node_attributes(G, get_information_centrality(G, weight='weight'), 'infc')

        if not 'name' in G.nodes[testNode].keys():
            print("   ...naming the nodes of the graph.")
            nodeNames={} ; nodeSegIDs={} ; nodeAtomNames={}
            for x in G.nodes():
                #i=dnad.nodesAtmSel[x].index
                nodeNames[x]  = name_node(dnad, x)
                nodeSegIDs[x] = dnad.nodesAtmSel[x].segid
                nodeAtomNames[x] = dnad.nodesAtmSel[x].name
            nodeNames = clarify_duplicate_nodes( nodeNames, nodeAtomNames )
            nx.set_node_attributes(G, nodeNames, "name")
            nx.set_node_attributes(G, nodeSegIDs, "segid")

        print("   ...finished checking additional graph information for window %i" % (w))_____no_output_____def encode_graph_format_edges(dnad):
    #pale = bokehPalettes.Viridis256
    #pMax = 256
    for w in range(dnad.numWinds):
        G=dnad.nxGraphs[w]
        vals = [ dnad.btws[w][x] for x in G.edges() ]
        vMin = np.min(vals) ; vMax=np.max(vals)

        edgeAlphasBtws={} ; edgeAlphasWeight={}
        edgeNames={}
        for x in G.edges():
            #edgeColors[x] = '#000000'
            r = (dnad.btws[w][x]-vMin)/(vMax-vMin)
            edgeAlphasBtws[x] = 0.1+0.9*r

        vals = [ G.edges[x]['weight'] for x in G.edges() ]
        vMin = np.min(vals) ; vMax=np.max(vals)
        for x in G.edges():
            r = (G.edges[x]['weight']-vMin)/(vMax-vMin)
            edgeAlphasWeight[x] = 0.1+0.9*r
            edgeNames[x]= "%s-%s" % (G.nodes[x[0]]['name'],G.nodes[x[1]]['name'])

        #nx.set_edge_attributes(G, edgeColors, "edge_color")
        nx.set_edge_attributes(G, edgeAlphasBtws, "edge_alpha")
        nx.set_edge_attributes(G, edgeAlphasBtws, "edge_alpha_btws")
        nx.set_edge_attributes(G, edgeAlphasWeight, "edge_alpha_weight")
        nx.set_edge_attributes(G, edgeNames, "name")

        # = = Set default edge width formats
        for a,b in G.edges():
            # = = = Assume increasing
            if a+1 == b:
                G.edges[a,b]['edge_width']=4
            else:
                G.edges[a,b]['edge_width']=1        _____no_output_____# Explanation of eigenvector centrality: http://olizardo.bol.ucla.edu/classes/soc-111/textbook/_book/6-6-sec-eigencent.html
listNodeProperties   = ['jsID','segid','communityID',
                        'eigenvector','btws','infc',
                        'bwcc','cwcc','wdeg','cohesion']
listNodePropertyNames= ['Index','Seg ID', 'Community ID',
                        'Eigenvector centrality','Node betweenness', 'Information centrality',
                        'Edge-betweenness weighted clustering coefficient','GCC weighted clustering coefficient','weighted degrees','Local cohesion']
listNodePropertiesPlot=['eigenvector','btws','infc',
                        'bwcc','cwcc','wdeg','cohesion']
listNodePropertiesPlotNames=['Eigenvector centrality', 'Node betweenness centrality', 'Information centrality',
                        'Edge-betweenness weighted clustering coefficient','GCC weighted clustering coefficient','weighted degrees','Local cohesion']_____no_output_____print("= = Check if graph has additional information not calculated in the default tutorial content.")
# = = Encode Neighbour information for Javascript referencing in the browser.
for w in range(dnad.numWinds):
    encode_neighbour_information(dnad.nxGraphs[w])

encode_additional_graph_data(dnad)

encode_graph_format_edges(dnad)_____no_output_____if bComparison:
    for w in range(dnadB.numWinds):
        encode_neighbour_information(dnadB.nxGraphs[w])
    encode_additional_graph_data(dnadB)
    encode_graph_format_edges(dnadB)_____no_output_____#dnad.nodesAtmSel[1300].index
# Undirected case: [G.edges(x) for x in G.nodes()]_____no_output_____#[ np.mean( [ dnad.nxGraphs[a].nodes[x]['bwcc'] for x in G.nodes() ] ) for a in range(dnad.numWinds) ]
#[ print( dnad.nxGraphs[a].nodes[0] ) for a in range(3) ]
#[ [ len(dnad.nodesComm[a]['commNodes'][x]) for x in dnad.nodesComm[0]['commLabels'] ] for a in range(3) ]_____no_output_____

### Compute the consensus graph based on each window_____no_output_____

def compute_consensus_graph(listG, node_properties, edge_properties):
    """
    The node_properties and edge properties are the values to take averages of.
    Assumes nodes are the same between all graphs in the list.
    Assumes that the union of all edges should be used, rather than the intersection of all edges.
    """
    bDoBWCC=False ; bDoCWCC=False
    nGraphs = len(listG)
    outG = nx.Graph()
    # = = = Create Nodes and Edges = = =
    for x, c in listG[0].nodes.data('name', default=None):
        outG.add_node(x, name=c, communityID=0)
    for w in range(nGraphs):
        for u, v, c in listG[w].edges.data('name', default=None):
            if (u,v) not in outG.edges():
                outG.add_edge(u,v,name=c)

    # = = = Evaluate Node Properties = = =
    for p in node_properties:
        if p == 'btws':
            nx.set_node_attributes(outG, nx.betweenness_centrality(outG), p)
        elif p == 'bwcc':
            bDoBWCC=True
        elif p == 'cwcc':
            bDoCWCC=True
        elif p == 'eigenvector':
            nx.set_node_attributes(outG, nx.eigenvector_centrality(outG), p)
        else:
            dictP={}
            for x in listG[0].nodes:
                dictP[x]=np.mean([ listG[w].nodes[x][p] for w in range(nGraphs) ])
            nx.set_node_attributes(outG, dictP, p)

    # = = = Evaluate Edge Properties = = =
    for p in edge_properties:
        if p == 'btws':
            nx.set_edge_attributes(outG, nx.edge_betweenness_centrality(outG), p)
        else:
            dictP={}
            for u,v in outG.edges():
                l=[ listG[w].edges[u,v][p] for w in range(nGraphs) if (u,v) in listG[w].edges() ]
                dictP[(u,v)]=np.mean(l)
            nx.set_edge_attributes(outG, dictP, p)

    if bDoBWCC:
        nx.set_node_attributes(outG, get_weighted_clustering_coefficient(outG, weight='btws', bRealSpace=True), 'bwcc')
    if bDoCWCC:
        nx.set_node_attributes(outG, get_weighted_clustering_coefficient(outG, weight='weight', bRealSpace=True), 'cwcc')

    return outG_____no_output_____#Gmean = compute_consensus_graph(dnad.nxGraphs, listNodePropertiesPlot, ['btws','dist','bwcc'])
#nx.set_node_attributes(Gmean, dict(dnad.nxGraphs[0].nodes.data('segid')), 'segid')_____no_output_____

## Setting the interactive viewer_____no_output_____### Pre-calculate Cartesian coordinates of the graphs in each window
Note: The distsAll key in DNAD is based on -log(Generalised Correlation Coefficient)
    ...we can try embedding using the actual atomic positions in the reduced trajectory._____no_output_____

def obtain_caretesian_distances(dnad):
    selfDistances = MDAdistances.self_distance_array(dnad.nodesAtmSel.positions)
    sqDistArr = np.zeros((dnad.numNodes, dnad.numNodes))
    triu = np.triu_indices_from(sqDistArr, k=1)
    sqDistArr[triu] = selfDistances
    sqDistArr.T[triu] = selfDistances
    return sqDistArr_____no_output_____def compute_scaled_cylindrical_coordinates(dnad, offsetTheta=None, bDict=False):
    """
    Remap the cartesian coordiantes of the trajectory into a 2D-space spanning (theta,z),
    with a fixed range (-1,1).
    Optional offset rotates theta so as to move the unwrapping point.
    """
    pos = dnad.nodesAtmSel.positions
    pos -= np.mean(pos,axis=0)
    x = np.arctan2(pos[...,1],pos[...,0])
    if offsetTheta is not None:
        x = (x + offsetTheta + np.pi) % (2 * np.pi) - np.pi
    y = np.interp(pos[...,2], (np.min(pos[...,2]), np.max(pos[...,2])), (-1.0, 1.0) )
    xy = np.stack( (x/np.pi,y), axis=-1 )
    if bDict:
        dictPos = {}
        for i, x in enumerate( dnad.nxGraphs[0].nodes() ):
            dictPos[x] = xy[i]
        return dictPos
    else:
        return xy

def compute_graph_embedding(distMat, posInit, bMetric=True):
    if bMetric:
        graphEmbedding = MDS(n_components=2, metric=bMetric,
                             max_iter=3000, eps=1e-9,
                             dissimilarity='precomputed', n_init=1)
    else:
        graphEmbedding = MDS(n_components=2, metric=False,
                             max_iter=3000, eps=1e-12,
                             dissimilarity="precomputed", n_init=1)
    graphEmbedding.fit_transform(distMat, init=posInit)
    print(graphEmbedding.embedding_.shape)
    return graphEmbedding.embedding_

def calculate_segID_cogs(G, pos, segIDList):
    nSegs=len(segIDList)
    nodeSegIDs = np.array( [ G.nodes[x]['segid'] for x in G.nodes() ] )
    cogSegs=np.zeros( (nSegs,2) )
    for i, seg in enumerate(segIDList):
        indices = np.where(nodeSegIDs==seg )[0]
        cogSegs[i] = np.mean( np.take(pos,indices,axis=0),axis=0)
    return cogSegs

def calculate_transform_matrix(cog):
    # First account for mirror imagert
    v1=(cog[3]+cog[1])-(cog[2]+cog[0])
    v2=(cog[0]+cog[1])-(cog[2]+cog[3])
    xx=1.0 if v1[0]>0.0 else -1.0
    yy=1.0 if v2[1]>0.0 else -1.0

    M1 = np.array([[xx,0],[0,yy]])
    cog = np.matmul(M1,cog.T).T

    # Rotate so that TD1 and TD2 remain directly above ND1 and ND2.
    v=(cog[0]+cog[1])-(cog[2]+cog[3])
    theta=0.5*np.pi-np.arctan2(v[1],v[0])
    print( "    ...rotation angle determined from vector:", theta*180.0/np.pi )
    M2=np.array([[np.cos(theta),-np.sin(theta)],[np.sin(theta),np.cos(theta)]])

    return np.matmul(M2, M1)

def standardise_coordinate_rotations(G, pos):
    # Identify the centers of geometry of the following SegIDS: TD1, TD2, ND1, ND2
    segIDList  = ['TD1', 'TD2', 'ND1', 'ND2']

    # First try to account for mirror imagery
    cogSegs=calculate_segID_cogs(G, pos, segIDList)
    TMat=calculate_transform_matrix(cogSegs)

    posOut = np.matmul(TMat,pos.T).T

    return posOut

def generate_average_node_positions(dnad):
    #if dnad.numWinds>1:
    #    distMat = np.zeros_like(dnad.distsAll[0])
    #    for w in range(dnad.numWinds):
    #        distMat += dnad.distsAll[w]
    #    distMat/=w
    #else:
    #    distMat=np.copy(dnad.distsAll[0])
    distMat=np.copy(dnad.distsAll[0])
    distMat[np.where(distMat<0)]=1.5*np.max(distMat)

    posEmbed = compute_graph_embedding(distMat, posNodesInit)
    #if dnad.numWinds>1:
    #    posEmbed = standardise_coordinate_rotations(dnad.nxGraphs[0], posEmbed)

    posNodesAverage={}
    for x in dnad.nxGraphs[0].nodes():
        posNodesAverage[x]=posEmbed[x]
    return posNodesAverage_____no_output_____posNodesAverage={} ; posNodesAverageB={}
if fileImportPos is not None:
    # = = = Cheat with resid by eliminating the first letter.
    refPosList={}
    with open(fileImportPos,'r') as fp:
        for line in fp:
            l=line.split()
            if len(l) != 3:
                continue
            refPosList[ l[0][1:] ] = [ float(l[1]), float(l[2]) ]
    G = dnad.nxGraphs[0]
    for n, name in G.nodes(data='name'):
        if name[1:] in refPosList.keys():
            posNodesAverage[n] = refPosList[name[1:]]
        else:
            posNodesAverage[n] = [0.0, 0.0]
    if bComparison:
        G = dnadB.nxGraphs[0]
        for n, name in G.nodes(data='name'):
            if name[1:] in refPosList.keys():
                posNodesAverageB[n] = refPosList[name[1:]]
            else:
                posNodesAverageB[n] = [0.0, 0.0]
    bPosSet=True
else:
    posNodesInit = compute_scaled_cylindrical_coordinates(dnad, offsetTheta=0)
    #posNodesInit[...,1].min()
    # Compute a single set of node position for all replicates, instead for one for each window
    bPosSet=False
_____no_output_____# Computed 2D embedding of atom positions based on the computed distance matrix in DNAD.
# Then rotate such that TMDs are on top and NBD1 is at the bottom-left hand corner
if not bPosSet:
    print("   ...Positions are not pre-set. Creating a 2D embedding based on the average GCC matrices.")
    bPosSet=True
    posNodesAverage = generate_average_node_positions(dnad)
    if bComparison:
        posNodesAverageB = generate_average_node_positions(dnadB)_____no_output_____# Encode a spatially aware neighbourhood information into the graph for future repositining.
for w in range(dnad.numWinds):
    encode_node_neighbours_with_spatial_ranking(dnad.nxGraphs[w], posNodesAverage, fieldID='jsID', fieldOutput='jsNeighOrdered')
if bComparison:
    for w in range(dnadB.numWinds):
        encode_node_neighbours_with_spatial_ranking(dnadB.nxGraphs[w], posNodesAverageB, fieldID='jsID', fieldOutput='jsNeighOrdered')_____no_output_____# = = = Custom code to modify and export node positions.
def read_cluster_definitions(fileName):
    dictOut={}
    with open(fileName,'r') as fp:
        for line in fp:
            l=line.split()
            if len(l)<2 or 'Intersection' in l[1]:
                continue
            k=l[0]
            if len(l)==2:
                dictOut[k] = []
            else:
                temp=[]
                for i in range(2,len(l)):
                    segid, resid = l[i].split(':')
                    temp.append( [segid, int(resid)] )
                dictOut[k] = temp
    return dictOut_____no_output_____# = = = Custom code to modify and export node positions.
if fileClusterDefinitions is not None:
    dictClusterDefs = read_cluster_definitions(fileClusterDefinitions)
    G = dnad.nxGraphs[0]
    for a, name in G.nodes(data='name'):
        # = = = Reassign ligands according to CoG of their edge neighbours?
        if 'Atp' in name or 'Mg' in name:
            posNew=np.zeros(2) ; count=0
            posNeighbours = [ posNodesAverage[b] for b in G.neighbors(a)
                              if  'h2o' not in G.nodes[b]['name']
                              and 'Atp' not in G.nodes[b]['name']
                              and 'Mg'  not in G.nodes[b]['name'] ]
            if len(posNeighbours) == 0:
                continue
            elif len(posNeighbours) == 1:
                continue
            else:
                posNew = np.mean(posNeighbours, axis=0)
                print( "%s (N=%i): %s -> %s" % (name, len(posNeighbours), posNew, posNodesAverage[a] ) )
                posNodesAverage[a] = posNew

        if 'h2o' in name:
            cID = name[3:]
            posNeighbours=[]
            for x in G.nodes():
                for info in dictClusterDefs[cID]:
                    if G.nodes[x]['segid'] == info[0] and int(G.nodes[x]['name'][1:]) == info[1]:
                         posNeighbours.append( posNodesAverage[x] )
                    continue
            posNew = np.mean(posNeighbours, axis=0)
            print( "%s (N=%i): %s -> %s" % (name, len(posNeighbours), posNew, posNodesAverage[a] ) )
            posNodesAverage[a] = posNew

if fileExportPos is not None:
    with open(fileExportPos,'w') as fp:
        for a, name in dnad.nxGraphs[0].nodes(data='name'):
            print( "%s %f %f" % ( name, posNodesAverage[a][0], posNodesAverage[a][1] ),
                  file=fp)_____no_output_____

### Build the contact matrix and compare the sort versus unsorted versions._____no_output_____

def get_nodemap_by_community_size(nodesComm):
    """
    example nodesComm: dnad.nodesComm[w]
    """
    d = dict() ; k=0
    for cID in nodesComm['commOrderSize']:
        for nodeID in np.sort(nodesComm['commNodes'][cID]):
            d[nodeID] = k
            k+=1
    return d

def get_reverse_dict(l):
    d = dict( enumerate(l) )
    return {v: k for k, v in d.items()}

def bin_edges_by_community(G, nodesComm):
    """
    Note that the graph node community IDs are already ordered by size.
    """
    #cMap = get_community_map_by_size(nodesComm )
    nComm = len( nodesComm['commOrderSize'] )
    outMap=np.zeros( (nComm,nComm), dtype=float)
    for u, v, weight in G.edges(data='weight'):
        #i = cMap[ G.nodes[u]['communityID'] ] ; j = cMap[ G.nodes[v]['communityID'] ]
        i = G.nodes[u]['communityID'] ; j = G.nodes[v]['communityID']
        outMap[i,j] += weight
        outMap[j,i] += weight
    return outMap

def bin_edges_by_segid(G):
    colourList = get_node_color_label_map(G, 'segid')
    cMap  = get_reverse_dict( colourList )
    nSegs = len(colourList)
    outMap=np.zeros( (nSegs,nSegs), dtype=float)
    for u, v, weight in G.edges(data='weight'):
        i = cMap[ G.nodes[u]['segid'] ] ; j = cMap[ G.nodes[v]['segid'] ]
        outMap[i,j] += weight
        outMap[j,i] += weight
    return outMap

def plot_bokehfigure_matrix(m, tickLabels=None):
    pSize=600
    offset=0.0
    if tickLabels is not None:
        offset=0.5
    source = bokehModels.ColumnDataSource(data=dict(x=[],y=[],z=[], l=[]))
    for i in range(m.shape[0]):
        for j in range(m.shape[1]):
            if m[i,j]>0:
                source.data['x'].append( i+offset )
                source.data['y'].append( j+offset )
                source.data['z'].append( m[i,j] )
                source.data['l'].append( "%.0f" % m[i,j] )
    colors = np.flip(bokehPalettes.Viridis256)
    mapper = bokehModels.LinearColorMapper(palette=colors, low=np.min(m), high=np.max(m))
    if tickLabels is None:
        p = bokehPlotting.figure(plot_width=pSize, plot_height=pSize)
    else:
        p = bokehPlotting.figure(plot_width=pSize, plot_height=pSize, x_range=tickLabels, y_range=tickLabels)

    p.rect(x="x", y="y", width=1, height=1, source=source, line_color=None, alpha=0.2,
           fill_color=bokehTransform.transform('z', mapper))
    labels = bokehModels.LabelSet(x="x", y="y", text="l", source=source,
               x_offset=0, y_offset=0, text_align='center', text_baseline='middle', render_mode='canvas')
    p.add_layout(labels)
    return p            _____no_output_____if False:
    dictTabGCC = OrderedDict()
    for w in range(dnad.numWinds):
        fig = plot_bokehfigure_matrix( bin_edges_by_community(dnad.nxGraphs[w], dnad.nodesComm[w]) )
        dictTabGCC['comm%i' % w] = bokehModels.Panel(child=fig, title="Communities, Window %s" % w)

    colourList = get_node_color_label_map(G, 'segid')
    for w in range(dnad.numWinds):
        fig = plot_bokehfigure_matrix( bin_edges_by_segid(dnad.nxGraphs[w]), tickLabels=colourList)
        dictTabGCC['seg%i' % w] = bokehModels.Panel(child=fig, title="SegIDs, Window %s" % w)

    tabContents = bokehModels.Tabs(tabs=[dictTabGCC[x] for x in dictTabGCC.keys()])
    show( tabContents )_____no_output_____def get_diamond_patch(x,y,xDev=0.707, yDev=0.707):
    return [ [x,x+xDev,x,x-xDev], [y+yDev,y,y-yDev,y] ]

def plot_bokehfigure_GCC(dnad, w):
    yScale=np.sqrt(2) ; xScale=1.0/np.sqrt(2) ; yyMin=0
    rangeX = (0,dnad.numNodes*xScale) ; rangeY = (0,dnad.numNodes*yScale)
    dictMap = get_nodemap_by_community_size(dnad.nodesComm[w])

    sourceA = bokehModels.ColumnDataSource(data=dict(x=[],y=[],z=[], name=[]))
    sourceB = bokehModels.ColumnDataSource(data=dict(x=[],y=[],z=[], name=[]))
    for u,v,z in dnad.nxGraphs[w].edges(data='weight'):
        f=1.0/np.sqrt(2)
        x = f*(u+v)*xScale ; y = f*(v-u)*yScale
        #sourceA.data['x'].append(x) ; sourceA.data['y'].append(y) ; sourceA.data['z'].append(z)
        xs, ys = get_diamond_patch(x, y, xScale/np.sqrt(2), yScale/np.sqrt(2))
        sourceA.data['x'].append(xs) ; sourceA.data['y'].append(ys) ; sourceA.data['z'].append(z)
        sourceA.data['name'].append(dnad.nxGraphs[w].edges[u,v]['name'])

        xx = f*(dictMap[u]+dictMap[v])*xScale ; yy = f*(dictMap[v]-dictMap[u])*yScale
        if yy>0:
            yy=-1*yy
        if yy < yyMin:
            yyMin=yy
        #sourceB.data['x'].append(xx) ; sourceB.data['y'].append(yy) ; sourceB.data['z'].append(z)
        xs, ys = get_diamond_patch(xx, yy, xScale/np.sqrt(2), yScale/np.sqrt(2))
        sourceB.data['x'].append(xs) ; sourceB.data['y'].append(ys) ; sourceB.data['z'].append(z)
        sourceB.data['name'].append(dnad.nxGraphs[w].edges[u,v]['name'])

    # this is the colormap from the original NYTimes plot
    #colors = ["#75968f", "#a5bab7", "#c9d9d3", "#e2e2e2", "#dfccce", "#ddb7b1", "#cc7878", "#933b41", "#550b1d"]
    colorsA = np.flip(bokehPalettes.YlOrRd[9])
    mapperA = bokehModels.LinearColorMapper(palette=colorsA, low=0, high=1)
    colorsB = np.flip(bokehPalettes.Blues[9])
    mapperB = bokehModels.LinearColorMapper(palette=colorsB, low=0, high=1)

    p = bokehPlotting.figure(plot_width=800, plot_height=400, title="Generalised Correlation Coefficients matrix",
                             x_axis_label='Node ID', y_axis_label='Separation', aspect_ratio = 1)
    #                        x_range=rangeX, y_range=rangeY)

    # = = = Triangle patches to separate communities.
    xPrev=0
    for cID in dnad.nodesComm[w]['commOrderSize']:
        cSize=len(dnad.nodesComm[w]['commNodes'][cID])
        p.patch(x=[xPrev,xPrev+0.5*cSize,xPrev+cSize],y=[0,-1*cSize,0],color="#EEEEEE",alpha=0.4)
        xPrev=xPrev+cSize
    #p.vbar(x=listX, width=0.2, bottom=yyMin, top=0, color="#CCCCCC")

    # = = = Individual plotes.
    #sizeSpec = {"value": 4, "units": "screen" }
    #sizeSpec = 1
    #p.rect(x="x", y="y", width=sizeSpec, height=sizeSpec, angle=np.pi/4, source=sourceA, line_color=None, fill_color=bokehTransform.transform('z', mapperA))
    #p.rect(x="x", y="y", width=sizeSpec, height=sizeSpec, angle=np.pi/4, source=sourceB, line_color=None, fill_color=bokehTransform.transform('z', mapperB))
    rendererA = p.patches(xs="x", ys="y", source=sourceA, line_color=None, fill_color=bokehTransform.transform('z', mapperA),
              legend_label='by sequence')
    rendererB = p.patches(xs="x", ys="y", source=sourceB, line_color=None, fill_color=bokehTransform.transform('z', mapperB),
              legend_label='sorted into communities')

    tooltips = [("Name:", "@name")]
    p.add_tools(bokehModels.HoverTool(tooltips=tooltips, renderers=[rendererA,rendererB]))
    p.toolbar.active_scroll = p.select_one(bokehModels.WheelZoomTool)

    #color_bar = bokehModels.ColorBar(color_mapper=mapperA,
    #                                 ticker=bokehModels.BasicTicker(desired_num_ticks=len(colors)),
    #                                 formatter=bokehModels.PrintfTickFormatter(format="%.2f"))
    #p.add_layout(color_bar, 'right')

    p.axis.axis_line_color = None
    p.axis.major_tick_line_color = None
    p.axis.major_label_text_font_size = "7px"
    p.axis.major_label_standoff = 0
    p.xaxis.major_label_orientation = 1.0

    #if not bPythonExport:
    #    output_notebook()
    #show(p)
    return p_____no_output_____import networkx.algorithms.community.quality as nxquality

# Creates a list of windows and order them according to graph modularity.
windModul = []
for w in range(dnad.numWinds):
    temp = OrderedDict()
    for k, v in dnad.nxGraphs[w].nodes(data='segid'):
        if v not in temp:
            temp[v]=set()
        temp[v].add(k)
    modul = nxquality.modularity(dnad.nxGraphs[w], [ temp[x] for x in temp ], weight="weight")
    windModul.append((w, modul))

windModul.sort(key=lambda x:x[1], reverse=True)

# Keep the window with the highest modularity as a reference for community matching
refWindow = windModul[0][0]

for w, mod in windModul:
    print( "Window {} has SegID modularity {:1.4f}.".format(w, mod) )_____no_output_____

### Map the community IDs of each window to the one with the highest modularity._____no_output_____

import networkx.algorithms.community.quality as nxquality

# Creates a list of windows and order them according to graph modularity.
windModul = []
for w in range(dnad.numWinds):
    modul = nxquality.modularity(dnad.nxGraphs[w],
                         [ set(nodesList) for nodesList in dnad.nodesComm[w]["commNodes"].values()])
    windModul.append((w, modul))

windModul.sort(key=lambda x:x[1], reverse=True)

# Keep the window with the highest modularity as a reference for community matching
refWindow = windModul[0][0]

for w, mod in windModul:
    print( "Window {} has modularity {:1.4f}.".format(w, mod) )_____no_output_____#print( dnad.nodesComm[w].keys() )
for w in range(dnad.numWinds):
    #print( [ len(dnad.nodesComm[w]['commNodes'][i]) for i in dnad.nodesComm[w]['commLabels'] ] )
    print( [ len(dnad.nodesComm[w]['commNodes'][x]) for x in dnad.nodesComm[w]['commOrderSize'] ] ) _____no_output_____

### Formatting: Define node and edge attributes for the renderer based on the data

Mostly definig a fized size and colour for each node property we want to show._____no_output_____

def get_node_data_range(G, nodeAttr):
    vals = [ G.nodes[x][nodeAttr] for x in G.nodes() ]
    return np.min(vals), np.max(vals)

def get_node_color_label_map(G, nodeAttr):
    vals = [ G.nodes[x][nodeAttr] for x in G.nodes() ]
    _, i = np.unique(vals, return_index=True)
    vMap=np.array([ vals[x] for x in np.sort(i)])
    return vMap

def format_graph_nodes_by_palette(G, nodeAttr, palette, sizeRange, nullColour='#FFFFFF'):
    # Set the node properties as additional entries in the graph.
    # Should I wrap palette around for text encodings that has more types than the number of colours in palette
    pMax = len(palette)
    nodeSizes={} ; nodeColors={}
    if type(G.nodes[0][nodeAttr])==str:
        vMap=get_node_color_label_map(G, nodeAttr)
        for x in G.nodes():
            i = np.where(vMap==G.nodes[x][nodeAttr])[0][0]
            if i>=pMax:
                nodeColors[x] = nullColour
            else:
                nodeColors[x] = palette[ i ]
            nodeSizes[x] = sizeRange
    else:
        vals = [ G.nodes[x][nodeAttr] for x in G.nodes() ]
        vMin = np.min(vals) ; vMax=np.max(vals)
        for x in G.nodes():
            r = (vals[x]-vMin)/(vMax-vMin)
            index = np.min( (int(r*pMax),pMax-1) )
            nodeColors[x] = palette[index]
            index = r*(sizeRange[1]-sizeRange[0])+sizeRange[0]
            nodeSizes[x] = index
    nx.set_node_attributes(G, nodeColors, "node_color_%s" % nodeAttr)
    #nx.set_node_attributes(G, nodeSizes, "node_size_%s" % nodeAttr)_____no_output_____colourPaletteLin = bokehPalettes.Viridis256
#FF0000 #0072B2 #E69F00 #F0E442 #009E73 #56B4E9 #D55E00 #CC79A7 #000000 #666666 #FFFFFF
colourPaletteCat = ['#FF0000'] + list( bokehPalettes.Colorblind[8] ) + ['#666666']
maxCommunityColours=20
colourPaletteCommunity=bokehPalettes.Category20[maxCommunityColours]
loneCommunityColour='#FFFFFF'
sizeCircleDefault=10
sizeCircleRangeFixed=(10,10)
sizeCircleRangeDefault=(8,16)
nodeAttrDefault="jsID"_____no_output_____for w in range(dnad.numWinds):
    G=dnad.nxGraphs[w]
    format_graph_nodes_by_palette(G, 'jsID', colourPaletteLin, sizeCircleRangeFixed)
    format_graph_nodes_by_palette(G, 'segid', colourPaletteCat, sizeCircleDefault)
    format_graph_nodes_by_palette(G, 'eigenvector', colourPaletteLin, sizeCircleRangeDefault)
    format_graph_nodes_by_palette(G, 'btws', colourPaletteLin, sizeCircleRangeDefault)
    format_graph_nodes_by_palette(G, 'infc', colourPaletteLin, sizeCircleRangeDefault)
    format_graph_nodes_by_palette(G, 'bwcc', colourPaletteLin, sizeCircleRangeDefault)
    format_graph_nodes_by_palette(G, 'cwcc', colourPaletteLin, sizeCircleRangeDefault)
    format_graph_nodes_by_palette(G, 'wdeg', colourPaletteLin, sizeCircleRangeDefault)
    format_graph_nodes_by_palette(G, 'cohesion', colourPaletteLin, sizeCircleRangeDefault)
if bComparison:
    for w in range(dnadB.numWinds):
        print("   ....running community ID assignment for window %i" % w)
        G=dnadB.nxGraphs[w]
        format_graph_nodes_by_palette(G, 'jsID', colourPaletteLin, sizeCircleRangeFixed)
        format_graph_nodes_by_palette(G, 'segid', colourPaletteCat, sizeCircleDefault)
        format_graph_nodes_by_palette(G, 'eigenvector', colourPaletteLin, sizeCircleRangeDefault)
        format_graph_nodes_by_palette(G, 'btws', colourPaletteLin, sizeCircleRangeDefault)
        format_graph_nodes_by_palette(G, 'infc', colourPaletteLin, sizeCircleRangeDefault)
        format_graph_nodes_by_palette(G, 'bwcc', colourPaletteLin, sizeCircleRangeDefault)
        format_graph_nodes_by_palette(G, 'cwcc', colourPaletteLin, sizeCircleRangeDefault)
        format_graph_nodes_by_palette(G, 'wdeg', colourPaletteLin, sizeCircleRangeDefault)
        format_graph_nodes_by_palette(G, 'cohesion', colourPaletteLin, sizeCircleRangeDefault)        _____no_output_____

#### Encode the Community ID into the network graph from the dnad community object, and reorder by decreasing size._____no_output_____

for w in range(dnad.numWinds):
    print("   ....running community ID assignment for window %i" % w)
    G=dnad.nxGraphs[w]
    # Note: These are the community IDs.
    #nColours=0
    #for x in dnad.nodesComm[w]['commLabels']:
    #    if len(dnad.nodesComm[w]['commNodes'][x]) > 1:
    #        nColours+=1
    k=0
    for i in dnad.nodesComm[w]['commOrderSize']:
        if len(dnad.nodesComm[w]['commNodes'][i]) > 1:
            if k>=maxCommunityColours:
                print(" = = WARNING: more than %i non-alone communitiess found! The palette is not enough." % maxCommunityColours)
                k=maxCommunityColours-1
            for j in dnad.nodesComm[w]['commNodes'][i]:
                dnad.nxGraphs[w].nodes[j]['communityID']=k
                dnad.nxGraphs[w].nodes[j]['node_color_communityID']=colourPaletteCommunity[k]
                #dnad.nxGraphs[w].nodes[j]['node_size_communityID']=sizeCircleDefault
            k+=1

        else:
            j=dnad.nodesComm[w]['commNodes'][i][0]
            dnad.nxGraphs[w].nodes[j]['communityID']=-1
            dnad.nxGraphs[w].nodes[j]['node_color_communityID']=loneCommunityColour
            #dnad.nxGraphs[w].nodes[j]['node_size_communityID']=sizeCircleDefault

    # = = Set default node format
    for x in G.nodes():
        G.nodes[x]['node_color']=G.nodes[x]['node_color_%s' % nodeAttrDefault]
        #G.nodes[x]['node_size']=G.nodes[x]['node_size_%s' % nodeAttrDefault]
        G.nodes[x]['node_size']=10_____no_output_____if bComparison:
    for w in range(dnadB.numWinds):
        print("   ....running community ID assignment for window %i" % w)
        G=dnadB.nxGraphs[w]
        # Note: These are the community IDs.
        #nColours=0
        #for x in dnadB.nodesComm[w]['commLabels']:
        #    if len(dnadB.nodesComm[w]['commNodes'][x]) > 1:
        #        nColours+=1
        k=0
        for i in dnadB.nodesComm[w]['commOrderSize']:
            if len(dnadB.nodesComm[w]['commNodes'][i]) > 1:
                if k>=maxCommunityColours:
                    print(" = = WARNING: more than %i non-alone communitiess found! The palette is not enough." % maxCommunityColours)
                    k=maxCommunityColours-1
                for j in dnadB.nodesComm[w]['commNodes'][i]:
                    dnadB.nxGraphs[w].nodes[j]['communityID']=k
                    dnadB.nxGraphs[w].nodes[j]['node_color_communityID']=colourPaletteCommunity[k]
                    #dnadB.nxGraphs[w].nodes[j]['node_size_communityID']=sizeCircleDefault
                k+=1

            else:
                j=dnadB.nodesComm[w]['commNodes'][i][0]
                dnadB.nxGraphs[w].nodes[j]['communityID']=-1
                dnadB.nxGraphs[w].nodes[j]['node_color_communityID']=loneCommunityColour
                #dnadB.nxGraphs[w].nodes[j]['node_size_communityID']=sizeCircleDefault

        # = = Set default node format
        for x in G.nodes():
            G.nodes[x]['node_color']=G.nodes[x]['node_color_%s' % nodeAttrDefault]
            #G.nodes[x]['node_size']=G.nodes[x]['node_size_%s' % nodeAttrDefault]
            G.nodes[x]['node_size']=10_____no_output_____print( G.nodes[0] )
print( G.edges(0) )
print( G.edges[0,1] )_____no_output_____

### Export some graph data for external analysis, e.g. visualisation in VMD_____no_output_____

# = = = Export data as needed
if True:
    export_graph_data(dnad, ['eigenvector','btws','cwcc', 'infc', 'cohesion'], prefix="graphdata_export_%s" % state)
    if bComparison:
        export_graph_data(dnadB, ['eigenvector','btws','cwcc', 'infc', 'cohesion'], prefix="graphdata_export_%s" % stateB)_____no_output_____

### Defining custom interactive elements such as updating selections_____no_output_____

#def create_JS_selection_update(sourceFrom, sourcePos, sourceTo, tableTo):
def create_JS_broadcast_selection_updates(sourceFrom, sLayoutProvider, sourceTargetNodes, sourceTargetNeighbours, sourceTargetEdges, tableTargetNodes, tableTargetNeighbours):
    """
    In-text example:
    graph_renderer.node_renderer.data_source.selected.js_on_change("indices", CustomJS(args=dict(,
    sFrom=graph_renderer.node_renderer.data_source  <- indices, names, segid
    pos=graph_renderer.layout_provider.graph_layout <- x,y
    sTo=sourceZoomNodes -> x,y, names, segid
    sAdj=sourceZoomAdjs -> x,y
    sEdge=sourceZoomEdges -> xs,ys
    table=table -> (none)
    """
    return bokehModels.CustomJS(args=dict(sFrom=sourceFrom,
                       #sEdgeFrom=sourceEdgeFrom,
                       sPos=sLayoutProvider,
                       sTo=sourceTargetNodes,
                       sAdj=sourceTargetNeighbours,
                       sEdge=sourceTargetEdges,
                       tTo=tableTargetNodes,
                       tAdj=tableTargetNeighbours),
        code="""
        var pos = sPos.graph_layout ;
        var inds = cb_obj.indices;
        var dFrom = sFrom.data;
        //var dEdgeFrom = sEdgeFrom.data;
        var dTo = sTo.data;
        var dAdj = sAdj.data;
        var dEdges = sEdge.data;
        var listAdj = [] ; var indsAddedAdj = []  ;
        var xThis = 0 ; var yThis = 0 ; var xAdj = 0 ; var yAdj = 0 ;
        dTo['x']     = [] ; dTo['y']      = [] ;
        dTo['name']  = [] ; dTo['segid']  = [] ;
        dTo['node_color']  = []
        dAdj['x']    = [] ; dAdj['y']     = [] ;
        dAdj['name'] = [] ; dAdj['segid'] = [] ;
        dAdj['node_color']  = []
        dEdges['xs'] = [] ; dEdges['ys']  = [] ;
        //dEdges['edge_alpha'] = [] ;
        for (var i = 0; i < inds.length; i++) {

            // Add position entries for node graph. Must keep all lists equal in length.
            xThis = pos[inds[i]][0] ; yThis = pos[inds[i]][1] ;
            dTo['x'].push(xThis) ; dTo['y'].push(yThis) ;

            // Add metadata entries for node table. Must keep all lists equal in length.
            dTo['name'].push( dFrom['name'][inds[i]] ) ; dTo['segid'].push( dFrom['segid'][inds[i]] )
            dTo['node_color'].push( dFrom['node_color'][inds[i]] ) ;

            // Add metadatra entries for node table. Must keep all lists equal in length.
            listAdj = dFrom['jsNeighbours'][inds[i]]
            for (var j = 0; j < listAdj.length; j++) {
                if ( !inds.includes(listAdj[j]) ) {
                    xAdj = pos[listAdj[j]][0] ; yAdj = pos[listAdj[j]][1] ;
                    dEdges['xs'].push([xThis,xAdj]) ; dEdges['ys'].push([yThis,yAdj]) ;
                    //dEdges['edge_alpha'].push( )
                    if( !indsAddedAdj.includes(listAdj[j]) ) {
                        indsAddedAdj.push( listAdj[j] )
                        dAdj['x'].push(xAdj) ; dAdj['y'].push(yAdj)
                        dAdj['name'].push( dFrom['name'][listAdj[j]] ) ; dAdj['segid'].push( dFrom['segid'][listAdj[j]] ) ;
                        dAdj['node_color'].push( dFrom['node_color'][listAdj[j]] ) ;
                    }

                }
            }
        }

        //Tell all target sources and widgets to update
        sTo.change.emit(); sAdj.change.emit();
        sEdge.change.emit();
        tTo.change.emit(); tAdj.change.emit();
    """,
    )      _____no_output_____def create_JS_update_format_nodes(source, listColourBar):
    """
    This Javascript snippet updates the node perperties and shows/hides the respective colourbar, depending on which property is being called for.
    """
    return bokehModels.CustomJS(args=dict(s1=source, lcb=listColourBar),
        code="""
        var d1 = s1.data;
        var f  = this.item;
        for (var i = 0; i < s1.length; i++) {
            d1['node_color'][i] = d1['node_color_'+f][i]
            // d1['node_size'][i] = d1['node_size_'+f][i]
        }
        s1.change.emit();
        for (var i = 0; i < lcb.length; i++) {
            if ( lcb[i].name == f ) {
                lcb[i].visible = true
            } else {
                lcb[i].visible = false
            }
        }
    """,
    )

def create_JS_update_format_edges(source):
    """
    This Javascript snippet updates the edge perperties, depending on which property is being called for.
    """
    return bokehModels.CustomJS(args=dict(s1=source),
        code="""
        var d1 = s1.data;
        var f  = this.item;
        //console.log(f, 'edge_alpha_'+f)
        for (var i = 0; i < s1.length; i++) {
            d1['edge_alpha'][i] = d1['edge_alpha_'+f][i]
        }
        s1.change.emit();
    """,
    )

def create_JS_search_attributes_and_select(sSearch, sNodes, sEdges, attr='name', charSplit=',', listCallbacks = []):
        return bokehModels.CustomJS(args=dict(sInput=sSearch, s=sNodes, sE=sEdges, attr=attr, charSplit=charSplit, listCB = listCallbacks), code="""
        var input  = sInput.value         ;
        var d      = s.data               ;
        var prevID = s.selected.indices   ;

        var list = input.split(charSplit)
        s.selected.indices = [] ;
        sE.selected.indices = [] ;
        for (var i = 0 ; i < list.length ; i++) {
            var j = d[attr].indexOf(list[i]) ;
            if ( j > -1 ) {
                s.selected.indices.push(j) ;
                //for (var k = 0; k < d['jsNeighbours'].length ; k++) {
                //    if ( ! d['jsNeighbours'][k] in sE.selected.indices ) {
                //       sE.selected.indices.push(d[attr]['jsNeighbours'][k]) ;
                //    }
                //}
            }
        }
        s.change.emit() ;
        // sE.change.emit() ;
        // Trigger downstream callbacks
        for (var i = 0 ; i < listCB.length ; i++) {
            listCB[i].execute(cb_obj = s.selected ) ;
        }
        """
        )

def create_JS_clone_selections(listSources, listCallbacks):
    return bokehModels.CustomJS(args=dict(listS = listSources, listCB = listCallbacks), code="""
    for (var i = 1 ; i < listCB.length ; i++) {
            listS[i].selected.indices = listS[0].selected.indices ;
            listCB[i].execute(cb_obj = listS[i].selected ) ;
    }
    """
    )_____no_output_____

### Circular Node arrangement Javascript_____no_output_____

def create_JS_update_reset_positions(sLayoutProvider, sNode, sEdge, pIn):
    return bokehModels.CustomJS(args=dict(sP=sLayoutProvider, s1=sNode, s2=sEdge, pIn=pIn),
        code="""
        var pos = sP.graph_layout ;
        Object.keys(pos).forEach(function(key) {
           pos[key] = pIn[key];
        });
        s1.change.emit();
        s2.change.emit();
    """,
    )

def create_JS_update_move_positions(sLayoutProvider, sNode, sEdge, attr, sSearch, sRadPower):
    return bokehModels.CustomJS(args=dict(sP=sLayoutProvider, s1=sNode, s2=sEdge, attr=attr, sInput=sSearch, sRP=sRadPower),
        code="""
        const defNone = -99.0;
        var pos = sP.graph_layout ;
        var d   = s1.data ;
        var nNodes = d['index'].length ;
        var plotted = [];
        var newPos = [];
        var xPos = 0; var yPos = 0; var n = 0;
        var radPower = sRP.value;

        function find_index_by_attr(d, attr, inp) {
            var i = d[attr].indexOf(inp) ;
            if (i > -1) { return i; } else { return Number.NaN ; }
        }
        function calc_ring_position( r, N ) {
            var out = [];
            for (var i=0; i<N; i++) { out.push( [r*Math.cos(6.28319*i/N), r*Math.sin(6.28319*i/N)] ) }
            return out;
        };
        function intersect(a1, a2) {
            const out = a1.filter(x => a2.indexOf(x) !== -1);
            return out;
        }
        function mean_angle(pos, list, def=0.0) {
            // Calculate based on mean position vectors, ather than mean angles.
            // Instead of returning theta from (-pi,pi), return from (0,2pi)
            var x = 0.0; var y = 0.0;
            if (list.length == 0) { return def ;}
            for (var i=0; i < list.length; i++) { x += pos[list[i]][0] ; y+=pos[list[i]][1]; }
            x/=list.length; y/=list.length;
            var th = Math.atan2(y, x);
            if ( th >= 0 ) { return th; } else { return th+6.28319; }
        }
        function sort_by(a, r){
            var s = []; var o=[];
            for (var i=0; i<a.length; i++) {s.push([a[i],r[i]]);}
            // low to high is a-b
            s.sort(function(a, b) {return a[1]-b[1];});
            for (var i=0; i<a.length; i++) {o.push(s[i][0]);}
            return o;
        }
        function get_remainder(pos, r) {
            var o = [];
            Object.keys(pos).forEach(function(k) {
                //WHY THE FUCK DOES JAVASCRIPT CAST THIS INTEGER KEY AUTOMATICALLY INTO STRING?
                var kk=parseInt(k);
                if (!(plotted.includes(kk))) { o.push(kk) };
            });
            return o;
        }
        function resolve_indirect_connections( dictRef, arr, ranking, def) {
            for (var i=0; i<arr.length; i++) {
                if ( ranking[i] == def) {
                    nShared = intersect(arr, dictRef[arr[i]]);
                    for ( var j=0;j<nShared.length;j++) {
                        if ( ranking[nShared[j]] != def ) { ranking[i] = ranking[nShared[j]] ; break; }
                    }
                }
            }
            return ranking;
        }

        var nCentre = find_index_by_attr( d, attr, sInput.value );
        if ( isNaN(nCentre) ) { return ; }
        pos[nCentre] = [0.0, 0.0] ;
        plotted.push( nCentre ) ;

        // First-ring neighbours
         var neigh1 = d['jsNeighOrdered'][nCentre]
        //var neigh1 = reorder_1st_ring_neighbours( nCentre, d['jsNeighOrdered'] );
        var neigh2 = [] ; var ranking = [] ; var tmp2 = [] ;
        // Set positions and downstream neighbours;
        newPos = calc_ring_position(1.0**radPower, neigh1.length);
        for (var i=0; i < neigh1.length; i++) {
            pos[ neigh1[i] ] = newPos[i] ;
            plotted.push( neigh1[i] ) ;
            for (var j=0; j<d['jsNeighOrdered'][neigh1[i]].length; j++ ) {
                n = d['jsNeighOrdered'][neigh1[i]][j]
                if ( !(plotted.includes(n))) {
                    tmp2.push( n ) ;
                }
            }
        }

        for (var i=0; i<tmp2.length; i++) {
            if ( !(plotted.includes(tmp2[i])) ) {
                if (!(neigh2.includes(tmp2[i]))) {
                    // Check connections to first-ring
                    var nShared = intersect(neigh1, d['jsNeighOrdered'][tmp2[i]]);
                    neigh2.push(tmp2[i]);
                    ranking.push(mean_angle(pos, nShared));
                }
            }
        }
        neigh2 = sort_by(neigh2, ranking);

        // = = Second-ring neighbours = =
        var neigh3 = [] ; var ranking = [] ; var tmp2 = [] ;
        // Set positions and neighbours;
        newPos = calc_ring_position(2.0**radPower, neigh2.length);
        for (var i=0; i < neigh2.length; i++) {
            pos[ neigh2[i] ] = newPos[i] ;
            plotted.push( neigh2[i] ) ;
            for (var j=0; j<d['jsNeighOrdered'][neigh2[i]].length; j++ ) {
                n = d['jsNeighOrdered'][neigh2[i]][j]
                if ( !(plotted.includes(n))) {
                    tmp2.push( n ) ;
                }
            }
        }

        for (var i=0; i<tmp2.length; i++) {
            if ( !(plotted.includes(tmp2[i])) ) {
                if (!(neigh3.includes(tmp2[i]))) {
                    // Check connections to first-ring
                    var nShared = intersect(neigh2, d['jsNeighOrdered'][tmp2[i]]);
                    neigh3.push(tmp2[i]);
                    ranking.push(mean_angle(pos, nShared));
                }
            }
        }
        neigh3 = sort_by(neigh3, ranking);

        // = = Third-ring neighbours = =
        // Set positions and neighbours;
        newPos = calc_ring_position(3.0**radPower, neigh3.length);
        for (var i=0; i < neigh3.length; i++) {
            pos[ neigh3[i] ] = newPos[i] ;
            plotted.push( neigh3[i] ) ;
        }

        // Outermost ring. Need to decide to hide?
        var neighZ = get_remainder(pos, plotted);
        for (var i=0; i<neighZ.length; i++) {
            pos[ neighZ[i] ] = [Number.NaN,Number.NaN] ;
        }

        //ranking = [];
        //for (var i=0; i<neighZ.length; i++) {
        //    nShared = intersect(neigh3, d['jsNeighOrdered'][neighZ[i]]);
        //    ranking.push(mean_angle(pos, nShared, defNone));
        //}
        //// Try to resolve indirect connections.
        //var i=0;
        //while ( ranking.includes(defNone) && i<10 ) {
        //    ranking = resolve_indirect_connections( d['jsNeighOrdered'], neighZ, ranking, defNone);
        //    i++;
        //}
        //neighZ = sort_by(neighZ, ranking);
        //newPos = calc_ring_position(6.0**radPower, neighZ.length);
        //for (var i=0; i<neighZ.length; i++) {
        //    pos[ neighZ[i] ] = newPos[i] ;
        //}

        s1.change.emit();
        s2.change.emit();
    """,
    )_____no_output_____

### Saving and loading positions_____no_output_____

def create_JS_update_load_positions(sLayoutProvider, sNode, sEdge, sourcePos, posDef=(0.0,0.0) ):
    return bokehModels.CustomJS(args=dict(sP=sLayoutProvider, s1=sNode, s2=sEdge, sPos=sourcePos, posDef=posDef),
        code="""
        // graph_layout uses jsID as keys, but our dictionary needs to have nodes names to enable transfer to between deletion mutants.
        function match_by_digits(k, d) {
            var n_kk = 0;
            var n_k = k.match(/\d+/)[0] ;
            Object.keys(d).forEach(function(kk) {
                n_kk = kk.match(/\d+/)[0] ;
                if ( n_k == n_kk ) { return kk; }
            });
            return '';
        }
        function find_index_by_attr(d, attr, inp) {
            var i = d[attr].indexOf(inp) ;
            if (i > -1) { return i; } else { return Number.NaN ; }
        }
        var pos = sP.graph_layout;
        var dictPos = sPos.data ;
        var keyNames = s1.data['name'] ;
        var k_name = '' ;
        if ( posDef == 'None' ) { posDef = (Number.NaN, Number.NaN) }

        //console.log(keyNames[0], pos[0], dictPos[keyNames[0]]) ;
        Object.keys(pos).forEach(function(k) {
            k_name = keyNames[k];
            if ( k_name in dictPos ) {
                pos[k] = dictPos[k_name] ;
            } else {
                var kAlt = match_by_digits(k_name, dictPos);
                if ( kAlt != '' ) {
                    pos[k] = dictPos[kAlt] ;
                } else {
                    pos[k] = posDef ;
                }
            }
        });
        s1.change.emit();
        s2.change.emit();
    """,
    )

def create_JS_update_save_positions(sLayoutProvider, sourceSharedPos, sourceData):
    return bokehModels.CustomJS(args=dict(sP=sLayoutProvider, sPos=sourceSharedPos, sD=sourceData),
        code="""
        // graph_layout uses jsID as keys, but our dictionary needs to have nodes names to enable transfer to between deletion mutants.
        var pos = sP.graph_layout;
        var dictPos = sPos.data ;
        var keyNames = sD.data['name'] ;
        Object.keys(dictPos).forEach(function(k) { dictPos[k] = [] ; });
        Object.keys(pos).forEach(function(k) {
            dictPos[keyNames[k]] = pos[k] ;
        });
        //console.log(keyNames[0], pos[0]) ;
    """,
    )_____no_output_____# Use a columnDataSource object to share temporary position data across all windows.
# This is done as Bokeh objects have existing JSON serialisation methods and CDS are interpreted as like Javascript dictionaries.
sharedPos = bokehModels.ColumnDataSource()_____no_output_____

## Finally prepare the plot_____no_output_____

def create_graph_visualisation_tab(G, listNodeProperties, titleTab="sim", bFirst=False, figureFirst=None, posNodes=None, sharedPos=None,
                                   colourPaletteLin=None, colourPaletteCat=None, colourPaletteCommunity=None, inputSearchbox=None):
    """
    Creates a self-contained analysis tab, and returns the following objects for interactive linking:
    - Tab itself.
    - the main figure object, for actions like range synchronisation
    - the graphRenderer of that figure
    - list of colour bars for interactive displays.
    - the update callback for synchronisation callbackBig

    """
    xRangeInit=yRangeInit=(-3,3)
    lineWidthDefault=1
    hoverColour='blue' ; lineWidthHover=4
    pWidth=800
    pHeight=700
    sbHeight=50
    tWidth=150 ; tHeight=int(0.5*(pHeight-sbHeight))
    wZoomFig=300                    ; hZoomFig=wZoomFig
    wZoomTableTitle=int(wZoomFig/2) ; hZoomTableTitle=50
    wZoomTab=int(wZoomFig/2)        ; hZoomTab=pHeight-hZoomFig-hZoomTableTitle
    sWidth=int(pWidth/2) ; sHeight=int(pHeight/2)
    ssWidth=int(pWidth/3)

    if bFirst:
        fig = bokehPlotting.figure(title=titleGraph,
                plot_width=pWidth, plot_height=pHeight,
                x_range=xRangeInit, y_range=yRangeInit,
                tools = "pan,wheel_zoom,box_zoom,box_select,tap,save,reset,help",
                toolbar_location='above')
    else:
        fig = bokehPlotting.figure(title=titleGraph,
              plot_width=pWidth, plot_height=pHeight,
              x_range=figureFirst.x_range, y_range=figureFirst.y_range,
              tools = "pan,wheel_zoom,box_zoom,box_select,tap,save,reset,help",
              toolbar_location='above')

    if posNodes is not None:
        graphRenderer = bokehPlotting.from_networkx(G, posNodes, scale=2, center=(0,0))
    else:
        graphRenderer = bokehPlotting.from_networkx(G, nx.spring_layout, scale=2, center=(0,0))
        posNodes = graph_renderer.layout_provider.graph_layout

    # Set up the tooltip on mouse hover here
    hoverTool1 = bokehModels.HoverTool(tooltips=[("name", "@name")], point_policy='snap_to_data')
    #hoverTool1 = bokehModels.HoverTool(tooltips=[("name", "@name"), ("%s" % nodeAttr,"@%s" % nodeAttr)], point_policy='snap_to_data')
    #hoverTool2 = bokehModels.HoverTool(renderers=[graphRenderer.edge_renderer],
    #                                  tooltips=[("name", "@name"), ("btws", "@btws")], point_policy='snap_to_data')
    #hoverTool = bokehModels.HoverTool(tooltips=[("name", "@name")], point_policy='snap_to_data')
    fig.add_tools(hoverTool1)
    #fig.add_tools(hoverTool2)
    # Turn Wheel Zoom on by default
    fig.toolbar.active_scroll = fig.select_one(bokehModels.WheelZoomTool)

    # = = FORMATTING COLOURBARS.
    listColourBars=[]
    for nodeAttr in listNodeProperties:
        if nodeAttr == 'segid':
            colourStyle = "node_color_%s" % nodeAttr
            #colourList = get_node_color_label_map(G, nodeAttr)[:len(colourPaletteCat)]
            colourList = get_node_color_label_map(G, nodeAttr)
            palette = colourPaletteCat
            if len(colourList) > len(palette):
                colourList = colourList[:len(palette)]
            elif len(palette) > len(colourList):
                palette = colourPaletteCat[:len(colourList)]
            colourMapper = bokehModels.mappers.CategoricalColorMapper(palette=palette, factors=colourList)
            colourBar = bokehModels.ColorBar(name=nodeAttr, color_mapper=colourMapper, label_standoff=12)
            fig.add_layout(colourBar, 'right')
        elif nodeAttr == 'communityID':
            colourStyle = "node_color_%s" % nodeAttr
            colourList = [str(x) for x in range(len(colourPaletteCommunity))]
            colourMapper = bokehModels.mappers.CategoricalColorMapper(palette=colourPaletteCommunity, factors=colourList)
            colourBar = bokehModels.ColorBar(name=nodeAttr, color_mapper=colourMapper, label_standoff=12, title=nodeAttr)
            fig.add_layout(colourBar, 'right')
        elif nodeAttr == 'annotation':
            colourStyle = "node_color_%s" % nodeAttr
            colourList = [k for k in colourMappingAnnotation.keys()]
            colourMapper = bokehModels.mappers.CategoricalColorMapper(palette=colourPaletteAnnotation, factors=colourList)
            colourBar = bokehModels.ColorBar(name=nodeAttr, color_mapper=colourMapper, label_standoff=12, title=nodeAttr)
            fig.add_layout(colourBar, 'right')
        else:
            cMin, cMax = get_node_data_range(G, nodeAttr)
            colourStyle = bokehTransform.linear_cmap(nodeAttr, colourPaletteLin,cMin,cMax)
            colourMapper = bokehModels.mappers.LinearColorMapper(palette=colourPaletteLin, low=cMin, high=cMax)
            colourBar = bokehModels.ColorBar(name=nodeAttr, color_mapper=colourMapper, label_standoff=12, title=nodeAttr)
            fig.add_layout(colourBar, 'right')
        colourBar.visible=False
        listColourBars.append(colourBar)
    for cb in listColourBars:
        if cb.name == nodeAttrDefault:
            cb.visible = True

    # = = = RENDERER Setup.
    #graphRenderer.node_renderer.glyph = bokehModels.Circle(size="node_size", fill_color=cMapTest, line_color='black', line_width=lineWidthDefault)
    graphRenderer.node_renderer.glyph = bokehModels.Circle(size="node_size", fill_color="node_color", line_width=lineWidthDefault)
    graphRenderer.node_renderer.nonselection_glyph = bokehModels.Circle(fill_alpha=0.0, line_alpha=0.0)
    graphRenderer.node_renderer.selection_glyph = bokehModels.Circle(size="node_size", fill_color="node_color", line_width=lineWidthHover)
    graphRenderer.node_renderer.hover_glyph = bokehModels.Circle(fill_color="node_color", line_width=lineWidthHover)

    #if nodeAttr == 'segid':
    #    vMap = get_node_color_label_map(G, nodeAttr )
    #    legend = bokehModels.Legend(items=[ (x, [graphRenderer.node_renderer.glyph]) for x in vMap ],
    #        location='top_left', orientation='horizontal',
    #        border_line_color="black", title='')
    #    dictFigures[nodeAttr].add_layout(legend, 'center')

    # help(dictFigures[nodeAttr].renderers[0].node_renderer.data_source)

    graphRenderer.edge_renderer.glyph = bokehModels.MultiLine(line_color="black", line_alpha="edge_alpha", line_width="edge_width")
    #graphRenderer.edge_renderer.nonselection_glyph = bokehModels.MultiLine(line_alpha=0.0)
    graphRenderer.edge_renderer.selection_glyph = bokehModels.MultiLine(line_color="black", line_alpha="edge_alpha", line_width=lineWidthHover)
    graphRenderer.edge_renderer.hover_glyph = bokehModels.MultiLine(line_color=hoverColour, line_alpha="edge_alpha", line_width=lineWidthHover)

    graphRenderer.selection_policy = bokehModels.NodesAndLinkedEdges()
    #graphRenderer.selection_policy = bokehModels.EdgesAndLinkedNodes()
    graphRenderer.inspection_policy = bokehModels.NodesAndLinkedEdges()
    #graphRenderer.inspection_policy = bokehModels.EdgesAndLinkedNodes()
    #graphRenderer.inspection_policy = bokehModels.NodesOnly()

    fig.renderers.clear()
    fig.renderers.append(graphRenderer)

    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    # = = Table column for selecting individual nodes
    # = = = = = = = = = = = = = = = = = = = = = = = = = = =

    dataColumns1 = [ bokehModels.TableColumn(field="name", title="Name", width=50) ]
    dataColumns1.append( bokehModels.TableColumn(field='segid', title="%s" % "SegID", width=100))
    #for nodeAttr in listNodeProperties:
    #    dataColumns1.append( bokehModels.TableColumn(field=nodeAttr, title="%s" % nodeAttr) )
    tableWidget1 = bokehModels.DataTable(source=graphRenderer.node_renderer.data_source, columns=dataColumns1,
            width=tWidth, height=tHeight, sortable=True, selectable=True, editable=False )

    #dataColumns2 = [ bokehModels.TableColumn(field="name", title="Name", width=50), bokehModels.TableColumn(field='btws', title='Edge betweenness centrality', width=100)]
    #tableWidget2 = bokehModels.DataTable(source=graphRenderer.edge_renderer.data_source, columns=dataColumns2,
    #        width=tWidth, height=tHeight, sortable=True, selectable=True, editable=False )

    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    # = = Zoom-in plots for a detailed view of selected nodes and their neighbours
    # = = = = = = = = = = = = = = = = = = = = = = = = = = =

    sourceZoomNodes = bokehModels.ColumnDataSource(data=dict(x=[], y=[], name=[], segid=[], node_color=[]))
    sourceZoomAdjs  = bokehModels.ColumnDataSource(data=dict(x=[], y=[], name=[], segid=[], node_color=[]))
    sourceZoomEdges = bokehModels.ColumnDataSource(data=dict(xs=[], ys=[]))

    figZoom = bokehPlotting.figure(title="Selection view", plot_width=wZoomFig, plot_height=hZoomFig,
                                      tools=["box_zoom", "wheel_zoom", "reset", "save"])

    glyphEdges = bokehModels.MultiLine(xs="xs", ys="ys", line_color="#000000", line_alpha=0.6, line_width=2)
    figZoom.add_glyph(sourceZoomEdges, glyphEdges)
    glyphAdj = figZoom.circle("x", "y", source=sourceZoomAdjs, size=10, alpha=0.6, color='node_color',
                           line_width=1, line_color='black')
    #glyphSel = figZoom.circle("x", "y", source=sourceZoomNodes, size=15, alpha=1, color='node_color',
    #                       line_width=1.5, line_color='black')
    glyphSel = figZoom.star(x="x", y="y", source=sourceZoomNodes, size=20, alpha=1, color='node_color',
                            line_width=1.5, line_color='black')

    tooltips = [("Name:", "@name"), ("SegID:", "@segid")]
    figZoom.add_tools(bokehModels.HoverTool(tooltips=tooltips, renderers=[glyphSel,glyphAdj]))
    figZoom.toolbar.active_scroll = figZoom.select_one(bokehModels.WheelZoomTool)

    # The div is here to provde a short title.
    divZoomNodes = bokehModels.Div( width=wZoomTableTitle, height=hZoomTableTitle, text="Selected Nodes" )
    columnsZoomNodes = [ bokehModels.TableColumn(field="name", title="Name", width=50), bokehModels.TableColumn(field="segid", title="SegID", width=100)]
    tableZoomNodes = bokehModels.DataTable(source=sourceZoomNodes, columns=columnsZoomNodes,
            width=wZoomTab, height=hZoomTab, sortable=True, selectable=False, editable=False )

    divZoomNeighbours = bokehModels.Div( width=wZoomTableTitle, height=hZoomTableTitle, text="Node Neighbours" )
    columnsZoomNeighbours = [ bokehModels.TableColumn(field="name", title="Name", width=50), bokehModels.TableColumn(field="segid", title="SegID", width=100)]
    tableZoomNeighbours = bokehModels.DataTable(source=sourceZoomAdjs, columns=columnsZoomNeighbours,
            width=wZoomTab, height=hZoomTab, sortable=True, selectable=False, editable=False )

    widgetZoomPackage = bokehLayouts.column(figZoom,
                                            bokehLayouts.row(bokehLayouts.column(divZoomNodes,tableZoomNodes),
                                                             bokehLayouts.column(divZoomNeighbours,tableZoomNeighbours),
                                                            )
                                           )

    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    # = = Link node selection from main plot to zoom-in plot and tables
    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    callbackBig = create_JS_broadcast_selection_updates(sourceFrom=graphRenderer.node_renderer.data_source,
        sLayoutProvider=graphRenderer.layout_provider,
        sourceTargetNodes=sourceZoomNodes,
        sourceTargetNeighbours=sourceZoomAdjs,
        sourceTargetEdges=sourceZoomEdges,
        tableTargetNodes=tableZoomNodes,
        tableTargetNeighbours=tableZoomNeighbours,
        )
    graphRenderer.node_renderer.data_source.selected.js_on_change("indices", callbackBig)

    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    # = = Button to select a subset based on node inputs.
    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    buttonSelect = bokehModels.Button(width=tWidth, label="Select nodes")
    buttonSelect.js_on_event(bokehEvents.ButtonClick, create_JS_search_attributes_and_select(
                                                                                sNodes=graphRenderer.node_renderer.data_source,
                                                                                sEdges=graphRenderer.edge_renderer.data_source,
                                                                                attr='name',
                                                                                sSearch=inputSearchbox,
                                                                                listCallbacks = [ callbackBig ] ) )

    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    # = = Widgets for repositioning nodes according to their local connections.
    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    sliderRadiusPower = bokehModels.Slider( width=tWidth, start=0.5, end=2, value=1, step=0.1, title="Radial sep.")

    buttonMovePos = bokehModels.Button(width=tWidth, label="Local view")
    buttonMovePos.js_on_event(bokehEvents.ButtonClick, create_JS_update_move_positions(graphRenderer.layout_provider,
                                                                            graphRenderer.node_renderer.data_source,
                                                                            graphRenderer.edge_renderer.data_source,
                                                                            'name',
                                                                            inputSearchbox,
                                                                            sliderRadiusPower))

    buttonSavePos = bokehModels.Button(width=tWidth, label="Save Shared Pos")
    buttonSavePos.js_on_event(bokehEvents.ButtonClick, create_JS_update_save_positions(graphRenderer.layout_provider, sharedPos, graphRenderer.node_renderer.data_source))
    buttonLoadPos = bokehModels.Button(width=tWidth, label="Load Shared Pos")
    buttonLoadPos.js_on_event(bokehEvents.ButtonClick, create_JS_update_load_positions(graphRenderer.layout_provider,
                                                                             graphRenderer.node_renderer.data_source,
                                                                             graphRenderer.edge_renderer.data_source,
                                                                             sharedPos,
                                                                             posDef = None ))

    buttonResetPos = bokehModels.Button(width=tWidth, label="Reset Positions")
    buttonResetPos.js_on_event(bokehEvents.ButtonClick, create_JS_update_reset_positions(graphRenderer.layout_provider,
                                                                             graphRenderer.node_renderer.data_source,
                                                                             graphRenderer.edge_renderer.data_source,
                                                                             posNodes))

    # = = FINAL TAB CONTENTS: Assemble all widgets into a single tab.
    tabContents=bokehLayouts.row(fig,
                                 bokehLayouts.column(buttonSelect,
                                                     buttonMovePos,
                                                     sliderRadiusPower,
                                                     buttonResetPos,
                                                     buttonSavePos,
                                                     buttonLoadPos,
                                                     tableWidget1
                                                    ),
                                 widgetZoomPackage,
                                )
    #tabContents=bokehLayouts.row( fig,
    #                              bokehLayouts.column(tableWidget1,tableWidget2),
    #                              )

    tab = bokehModels.Panel(child=tabContents, title=titleTab)

    return tab, fig, graphRenderer, listColourBars, callbackBig_____no_output_____# Define plot with starting X and Y ranges, kind of tools available, etc.
lineWidthDefault=1
hoverColour='blue' ; lineWidthHover=4

pWidth=800
pHeight=700
sbHeight=50
tWidth=150 ; tHeight=int(0.5*(pHeight-sbHeight))
wZoomFig=300                    ; hZoomFig=wZoomFig
wZoomTableTitle=int(wZoomFig/2) ; hZoomTableTitle=50
wZoomTab=int(wZoomFig/2)        ; hZoomTab=pHeight-hZoomFig-hZoomTableTitle
sWidth=int(pWidth/2) ; sHeight=int(pHeight/2)
ssWidth=int(pWidth/3)

# = = = = = = = = = = = = = = = = = = = = = = = = = = =
# = = Widgets to link node and edge properties across all DNA windows: Initial declaration.
# = = = = = = = = = = = = = = = = = = = = = = = = = = =

# = = SIZE, EDGE, AND POSITION PROPERTIES = = =
#spinnerWidget1 = bokehModels.Slider(title='Node size', start=0, end=20, step=0.5, value=sizeCircleDefault)
spinnerWidget1 = bokehModels.Spinner(title='Node size', low=0, high=20, step=0.5, value=sizeCircleDefault, width_policy='min')
spinnerWidget2 = bokehModels.Spinner(title='Edge width', low=0, high=10, step=0.5, value=lineWidthDefault, width_policy='min')
spinnerWidget3 = bokehModels.Spinner(title='Selection line width', low=0, high=10, step=0.5, value=lineWidthHover, width_policy='min')
#spinnerWidget4 = bokehModels.Spinner(title='Window-based node positions', low=0, high=1, step=0.05, value=0.0, width_policy='min')

# = = = = = = = = = = = = = = = = = = = = = = = = = = =
# = = Text input selection for manual selection of residues.
# = = = = = = = = = = = = = = = = = = = = = = = = = = =
#textBanner = Paragraph(text=welcome_message, width=200, height=100)
inputSearchbox = bokehModels.TextInput(value="", title="Search by name(s):", width=wZoomFig )
# Can use syntax (Name, Name,...)
#inputSearchbox.js_on_change('value', create_JS_search_attributes_and_select(source=graphRenderer.node_renderer.data_source,

widgetSliders = bokehLayouts.row([spinnerWidget1,spinnerWidget2,spinnerWidget3, inputSearchbox])

# = = = = = = = = = = = = = = = = = = = = = = = = = = =
# = = NODE-BASED COLOUR CONTROLS = = =
# = = = = = = = = = = = = = = = = = = = = = = = = = = =
#dictButtons={}
#for nodeAttr in listNodeProperties:
#    b = bokehModels.Button(width_policy='min', label="format by %s" % nodeAttr)
#    dictButtons[nodeAttr] = b
#widgetColourControls=bokehLayouts.row([ dictButtons[x] for x in dictButtons.keys() ])
menuNodeColourControls = []
for name, attr in zip(listNodePropertyNames, listNodeProperties):
    menuNodeColourControls.append( (name, attr) )
dropdownNodeColourControls = bokehModels.Dropdown(label="Colour nodes by...", button_type="default", width_policy='min',
                                                  background="#ddddee", menu=menuNodeColourControls)

menuEdgeColourControls = [('Edge-betweenness','btws'),('Gen. correlation coefficient','weight')]
dropdownEdgeColourControls = bokehModels.Dropdown(label="Shade edges by...", button_type="default", width_policy='min',
                                                  background="#ddddee", menu=menuEdgeColourControls)

buttonCloneSelection = bokehModels.Button(width_policy='min', label="Apply Window 0 selection to all windows.")

widgetColourControls = bokehLayouts.row( [dropdownNodeColourControls,dropdownEdgeColourControls,buttonCloneSelection] )

# = = = TABS Setup for each window
bFirst=True; figFirst=None
dictTab={}
listGraphNodeSources = [] ; listCallbackBigs = []
for w in range(dnad.numWinds):
    label="%s_%i" % (state, w)
    G = dnad.nxGraphs[w]
    tab, fig, graphRenderer, listColourBars, callbackBig = create_graph_visualisation_tab(G,
                                    listNodeProperties,
                                    titleTab=label,
                                    bFirst=bFirst,
                                    figureFirst=figFirst,
                                    posNodes=posNodesAverage,
                                    sharedPos=sharedPos,
                                    colourPaletteLin=colourPaletteLin,
                                    colourPaletteCat=colourPaletteCat,
                                    colourPaletteCommunity=colourPaletteCommunity,
                                    inputSearchbox=inputSearchbox)

    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    # = = Widget Javascripts to link node and edge properties across all graph tabs
    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    dropdownNodeColourControls.js_on_event("menu_item_click",
                                           create_JS_update_format_nodes(graphRenderer.node_renderer.data_source, listColourBars))
    dropdownEdgeColourControls.js_on_event("menu_item_click",
                                           create_JS_update_format_edges(graphRenderer.edge_renderer.data_source))

    #spinnerWidget1 = bokehModels.Slider(title='Node size', start=0, end=20, step=0.5, value=12, width=ssWidth, width_policy='fit')
    spinnerWidget1.js_link('value_throttled', graphRenderer.node_renderer.glyph, 'size')
    #spinnerWidget2 = bokehModels.Slider(title='Edge width', start=0, end=10, step=0.5, value=lineWidthDefault, width=ssWidth, width_policy='fit')
    spinnerWidget2.js_link('value_throttled', graphRenderer.edge_renderer.glyph, 'line_width')
    #spinnerWidget3 = bokehModels.Slider(title='Selection line width', start=0, end=10, step=0.5, value=lineWidthHover, width=ssWidth, width_policy='fit')
    spinnerWidget3.js_link('value_throttled', graphRenderer.node_renderer.selection_glyph, 'line_width')
    spinnerWidget3.js_link('value_throttled', graphRenderer.edge_renderer.selection_glyph, 'line_width')

    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    # = = Widget Javascripts to link selection of nodes across graph tabs. Gather step
    # = = = = = = = = = = = = = = = = = = = = = = = = = = =
    # = = = Selection synchronizing WIP
    listGraphNodeSources.append(graphRenderer.node_renderer.data_source)
    listCallbackBigs.append(callbackBig)

    dictTab[label] = tab
    if bFirst:
        bFirst=False ; figFirst=fig

# = = = Load comparisons as well, assuming it it possible
if bComparison:
    for w in range(dnadB.numWinds):
        label="%s_%i" % (stateB, w)
        G = dnadB.nxGraphs[w]
        tab, fig, graphRenderer, listColourBars, callbackBig = create_graph_visualisation_tab(G,
                                        listNodeProperties,
                                        titleTab=label,
                                        bFirst=bFirst,
                                        figureFirst=figFirst,
                                        posNodes=posNodesAverageB,
                                        sharedPos=sharedPos,
                                        colourPaletteLin=colourPaletteLin,
                                        colourPaletteCat=colourPaletteCat,
                                        colourPaletteCommunity=colourPaletteCommunity,
                                        inputSearchbox=inputSearchbox)

        # = = = = = = = = = = = = = = = = = = = = = = = = = = =
        # = = Widget Javascripts to link node and edge properties across all graph tabs
        # = = = = = = = = = = = = = = = = = = = = = = = = = = =
        dropdownNodeColourControls.js_on_event("menu_item_click",
                                               create_JS_update_format_nodes(graphRenderer.node_renderer.data_source, listColourBars))
        dropdownEdgeColourControls.js_on_event("menu_item_click",
                                               create_JS_update_format_edges(graphRenderer.edge_renderer.data_source))

        #spinnerWidget1 = bokehModels.Slider(title='Node size', start=0, end=20, step=0.5, value=12, width=ssWidth, width_policy='fit')
        spinnerWidget1.js_link('value_throttled', graphRenderer.node_renderer.glyph, 'size')
        #spinnerWidget2 = bokehModels.Slider(title='Edge width', start=0, end=10, step=0.5, value=lineWidthDefault, width=ssWidth, width_policy='fit')
        spinnerWidget2.js_link('value_throttled', graphRenderer.edge_renderer.glyph, 'line_width')
        #spinnerWidget3 = bokehModels.Slider(title='Selection line width', start=0, end=10, step=0.5, value=lineWidthHover, width=ssWidth, width_policy='fit')
        spinnerWidget3.js_link('value_throttled', graphRenderer.node_renderer.selection_glyph, 'line_width')
        spinnerWidget3.js_link('value_throttled', graphRenderer.edge_renderer.selection_glyph, 'line_width')

        # = = = = = = = = = = = = = = = = = = = = = = = = = = =
        # = = Widget Javascripts to link selection of nodes across graph tabs. Gather step
        # = = = = = = = = = = = = = = = = = = = = = = = = = = =
        # = = = Selection synchronizing WIP
        listGraphNodeSources.append(graphRenderer.node_renderer.data_source)
        listCallbackBigs.append(callbackBig)

        dictTab[label] = tab_____no_output_____# = = = = = = = = = = = = = = = = = = = = = = = = = = =
# = = Widget Javascripts to link selection of nodes across graph tabs. Call widget
# = = = = = = = = = = = = = = = = = = = = = = = = = = =
buttonCloneSelection.js_on_click( create_JS_clone_selections(listGraphNodeSources, listCallbackBigs) )_____no_output_____#print( graphRenderer.__dict__.keys() )
#graphRenderer.node_renderer.data_source.data.keys()
#graphRenderer.edge_renderer.data_source.data.keys()
#colourBar.name
#tableWidget1.source.data.keys()
#graphRenderer.layout_provider.properties()_____no_output_____# = = Linking Node positions = = Note: currently doesn't work even though the positions are updated.
if False:
    dictGraphPos={} ; dictGraphLayout={}
    for w in range(dnad.numWinds):
        dictGraphLayout[w]=graphRenderer.layout_provider
        dictGraphPos[w]=graphRenderer.layout_provider.graph_layout

    spinnerWidget4.js_property_callbacks={}
    spinnerWidget4.js_on_change('value_throttled',
        bokehModels.CustomJS(args=dict(pMean=posNodesAverage,
                                       pWindow=posNodes,
                                       pTarget=dictGraphPos,
                                       gTarget=dictGraphLayout,
                                       ),
        code="""
            var v = cb_obj.value_throttled ;
            console.log('init:', pTarget[0][0])
            for (var w in pTarget ) {
                for (var i in pTarget[w] ) {
                    pTarget[w][i][0] = pMean[i][0]+v*(pWindow[w][i][0]-pMean[i][0])
                    pTarget[w][i][1] = pMean[i][1]+v*(pWindow[w][i][1]-pMean[i][1])
                }
                gTarget[w].change.emit;
            }
            console.log('final:', pTarget[0][0])
        """,
        ))_____no_output_____#selectWidget = bokehModels.Select(title="Select Colour Map", value="node_color_bwcc",
#                options = ["node_color_bwcc","node_color_eigenvector", "node_color_segid"])
#selectWidget.js_link('value', graphRenderer.node_renderer.glyph, 'fill_color')
#graphRenderer.node_renderer.data_source.column_names_____no_output_____#controlWidget = bokehLayouts.WidgetBox(selectWidget)_____no_output_____

## Create a separate tab dedicated to graphical analysis_____no_output_____### Sequence-based graphs_____no_output_____

colourPaletteLines = bokehPalettes.Colorblind[8]
#listNodeProperties=['segid','communityID','bwcc','eigenvector', 'btws']

def generate_plot_data_source(dnad):
    s = bokehModels.ColumnDataSource(data=dict())
    listNodeIDs = [ x for x in dnad.nxGraphs[0].nodes() ]
    s.data['x']     = listNodeIDs
    s.data['name']  = [ dnad.nxGraphs[0].nodes[x]['name'] for x in listNodeIDs ]
    s.data['segid'] = [ dnad.nxGraphs[0].nodes[x]['segid'] for x in listNodeIDs ]
    return s

TOOLTIPS = [("name", "@name"),("segid", "@segid")]

bFirst=True
listGraphs=[] ; dictHT={}
for nodeAttr, nodeAttrName in zip(listNodePropertiesPlot,listNodePropertiesPlotNames):
    if bFirst:
        plotAttr = bokehPlotting.Figure(plot_width=1000, plot_height=250, x_axis_label='Node ID', y_axis_label=nodeAttr,
                                   title=nodeAttrName, tooltips=TOOLTIPS, tools = "pan,xwheel_zoom,box_zoom,save,reset,help")
        pFirst=plotAttr
    else:
        plotAttr = bokehPlotting.Figure(plot_width=1000, plot_height=250, x_axis_label='Node ID', y_axis_label=nodeAttr, x_range=pFirst.x_range,
                                   title=nodeAttrName, tooltips=TOOLTIPS, tools = "pan,xwheel_zoom,box_zoom,save,reset,help")
    plotAttr.toolbar.active_scroll = plotAttr.select_one(bokehModels.WheelZoomTool)
    dataBlock = extract_node_data(dnad, nodeAttr)
    plotSource = generate_plot_data_source(dnad)
    # = = Plot per window
    if not bComparison:
        for w in range(dnad.numWinds):
            yStr='y%i' % w
            plotSource.data['y%i'%w]=dataBlock[w]
            renderer = plotAttr.line('x', yStr, legend_label='window %i' % w, source=plotSource,line_width=1, line_color=colourPaletteLines[w])
            renderer.visible = True

    # = = Plot mean values
    mean=np.mean(dataBlock,axis=0); std=np.std(dataBlock,axis=0)
    plotSource.data['mean']=mean
    plotSource.data['lower']=mean-std
    plotSource.data['upper']=mean+std
    renderer  = plotAttr.line('x', 'mean', legend_label='mean', source=plotSource,line_width=1, line_color='black')
    #rendererE = bokehModels.Whisker(source=plotSource,
    #                        base="x", upper="upper", lower="lower",line_width=1, line_color='black')
    #plotAttr.add_layout( rendererE )
    if not bComparison:
        renderer.visible  = False

    # Plot mean wildtype values
    if bComparison:
        plotSourceB = generate_plot_data_source(dnadB)
        dataBlockB = extract_node_data(dnadB, nodeAttr)
        mean=np.mean(dataBlockB,axis=0); std=np.std(dataBlockB,axis=0)
        plotSourceB.data['mean']=mean
        plotSourceB.data['lower']=mean-std
        plotSourceB.data['upper']=mean+std
        renderer = plotAttr.line('x', 'mean', legend_label='mean (B)', source=plotSourceB, line_width=1, line_color='grey')
        #rendererEB = bokehModels.Whisker(source=plotSourceB,
        #                        base="x", upper="upper", lower="lower",line_width=1, line_color='grey')
        #plotAttr.add_layout( rendererEB )

    plotAttr.legend.location = "top_left"
    plotAttr.legend.click_policy="hide"
    listGraphs.append(plotAttr)
    bFirst=False

tabContents = bokehLayouts.column(listGraphs)

dictTab['graph'] = bokehModels.Panel(child=tabContents, title="Graphs")_____no_output_____

### GCC Graph (WIP)_____no_output_____

if False:
    dictTabGCC = OrderedDict()
    for w in range(dnad.numWinds):
        fig = plot_bokehfigure_GCC(dnad, w)
        dictTabGCC[w] = bokehModels.Panel(child=fig, title="Window %s" % w)
    #colourPaletteCat
    #colourPaletteCommunity
    tabContents = bokehModels.Tabs(tabs=[dictTabGCC[x] for x in dictTabGCC.keys()])
    dictTab['GCC'] = bokehModels.Panel(child=tabContents, title="GCC matrix")_____no_output_____

## Show or export the final analysis interface_____no_output_____

output_file(outputFileName)
if not bPythonExport:
    output_notebook()_____no_output_____#show(bokehModels.Tabs(tabs=[dictTab[x] for x in dictTab.keys()]))
show(bokehLayouts.column(widgetSliders,widgetColourControls,bokehModels.Tabs(tabs=[dictTab[x] for x in dictTab.keys()])))_____no_output_____