diff --git a/.github/worflows/doxygen-pdf.yml b/.github/workflows/doxygen-pdf.yml similarity index 100% rename from .github/worflows/doxygen-pdf.yml rename to .github/workflows/doxygen-pdf.yml diff --git a/docs/HIVTxNetwork.html b/docs/HIVTxNetwork.html index 8064b61..31e3ca8 100644 --- a/docs/HIVTxNetwork.html +++ b/docs/HIVTxNetwork.html @@ -1,2026 +1,5315 @@ - - - - - Documentation Class: HIVTxNetwork + + + JSDoc: Class: HIVTxNetwork + + - - - - - - - + + + -
-
-
-
-

Class: HIVTxNetwork

-
-
-

HIVTxNetwork

-
- -
-
-
-
-

- new HIVTxNetwork(json, - button_bar_ui, cluster_attributes) -

-
-
-
- Represents an HIV transmission network with annotations -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
json - Object - - A JSON object containing the network data. -
button_bar_ui - HTMLElement - - A UI element for interacting with the network. -
cluster_attributes - Object - - Attributes related to clusters within the network. -
- -
-
-
- -

Members

- -
-
-
-

- _calc_country_nodes -

-
-
-
- this is a function which calculates country node centers - for the (experimental) option of rendering networks with - topo maps -
- -
-
- -
-
-

- auto_expand_pg_handler -

-
-
-
- Grow a CoI defined in @pg based on its growth mode -
- -
-
- -
-
-

- charge_correction -

-
-
-
d3 layout option setting
- -
-
- -
-
-

- check_for_time_series -

-
-
-
- Generate a function callback for attribute time series - data -
- -
-
- -
-
-

- cluster_filtering_functions -

-
-
-
- filters which control which clusters get rendered -
- -
-
- -
-
-

- colorizer -

-
-
-
default node colorizer
- -
-
- -
-
-

- defined_priority_groups -

-
-
-
- the list of defined clusters of interest, format as - follows (SLKP, 20240715: may need updating) { 'name' : - 'unique name', 'nodes' : [ { 'node_id' : text, 'added' : - date, 'kind' : text }], 'created' : date, 'description' : - 'text', 'modified' : date, 'kind' : 'text' } -
- -
-
- -
-
-

- filter_by_size -

-
-
-
-
- -
-
-

- filter_if_added -

-
-
-
-
- -
-
-

- filter_singletons -

-
-
-
-
- -
-
-

- filter_time_period -

-
-
-
-
- -
-
-

- highlight_unsuppored_edges -

-
-
-
- if there is computed support for network edges, use it to - highlight possible spurious edges -
- -
-
- -
-
-

- node_label_drag -

-
-
-
- define a D3 behavior to make node labels draggable -
- -
-
- -
-
-

- node_shaper -

-
-
-
default node shaper
- -
-
- -
-
-

- priority_groups_all_events -

-
-
-
- generate a set of all unique temporal events (when new - data were added to ANY CoI) return a Set of date strings - formatted with timeDateUtil.DateViewFormatSlider -
- -
-
- -
-
-

- priority_groups_compute_overlap -

-
-
-
- compute the overlap between CoI -
- -
-
- -
-
-

- priority_groups_edit_set_description -

-
-
-
- A function that updates the "freehand" description of a - specific CoI -
- -
-
- -
-
-

- priority_groups_export -

-
-
-
- export CoI records for interactions with the external DB -
- -
-
- -
-
-

- priority_groups_export_nodes -

-
-
-
- Export nodes that are members of CoI -
- -
-
- -
-
-

- priority_groups_export_sets -

-
-
-
Export CoI summary info
- -
-
- -
-
-

- priority_groups_find_by_name -

-
-
-
- lookup a CoI by name; null if not found -
- -
-
- -
-
-

- priority_groups_is_new_node -

-
-
-
- returns true is the node was added by the system during - CoI definition/expansion -
- -
-
- -
-
-

- priority_groups_remove_set -

-
-
-
- Remove a CoI from the list of defined CoI -
- -
-
- -
-
-

- priority_groups_update_node_sets -

-
-
-
- interact with the remote DB to send updates of CoI - operations -
- -
-
- -
-
-

- using_time_filter -

-
-
-
- time filter element for various displays -
- -
-
-
- -

Methods

- -
-
-
-

- annotate_cluster_changes() -

-
-
-
- annotate_cluster_changes If the network contains - information about cluster changes (new/moved/deleted - nodes, etc), this function will annotate cluster objects - (in place) with various attributes "delta" : change in the - size of the cluster "flag" : a status flag to be used in - the cluster display table if set to 2 then TBD if set to 3 - then TBD -
- -
-
- -
-
-

- attribute_node_value_by_id(d:, id:, number:) -

-
-
-
- Fetch the value of an attribute from the node -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
d:node object
id: - [string] the attribute whose value should be fetched -
number: - [bool] if true, only return numerical values -
- -
-
- -
-
-

- cluster_display_filter() -

-
-
-
-
- -
-
-

- define_attribute_COI_membership(network, data:) -

-
-
-
- define an attribute generator for subcluster membership - attribute -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - + - +
-
- - -
NameTypeDescription
network - : the network / cluster object to ise -
data: - reference date to use -
+

Class: HIVTxNetwork

-
+ -
Returns:
-
attribute definition
-
- -
-
-

- define_attribute_binned_vl(field:, title:) -

-
-
-
- define an attribute generator for binned viral loads -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
field: - the node attribute field to use -
title: - display this title for the attribute -
- -
-
Returns:
-
attribute definition dict
-
+
-
-
-

- define_attribute_dx_years(relative:, label:) -

-
-
-
- define an attribute generator for dx year -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
relative: - if T, compute dx date relative to the network date - in years -
label:use this label
- -
- -
Returns:
- -
attribute definition dict
-
- -
-
-

- define_attribute_network_update() -

-
-
-
- define an attribute generator for new network - nodes/clusters -
- -
- -
Returns:
- -
attribute definition dict
-
- -
-
-

- define_attribute_vl_interpretaion() -

-
-
-
- define an attribute generator for Viral load result - interpretatio -
- -
- -
Returns:
- -
attribute definition dict
-
- -
-
-

- display_warning() -

-
-
-
display a warning string
- -
-
- -
-
-

- extract_single_cluster(nodes, filter, no_clone) -

-
-
-
- Extract the nodes and edges between them into a separate - object -
- -
Parameters:
- - - - - - - - - - - - - - - +
+ +

HIVTxNetwork(json, button_bar_ui, cluster_attributes)

+ + +
- +
+
+ + -
- + - - + +

new HIVTxNetwork(json, button_bar_ui, cluster_attributes)

+ - - - - - - - - - - - - - -
NameTypeDescription
nodes - [array] the list of nodes to extract -
filter - [function, optional] (edge) -> bool filtering - function for deciding which edges will be used to - define clusters -
no_clone - [bool] if set to T, node objects are **not** shallow - cloned in the return object -
- -
- -
Returns:
- -
- [dict] the object representing "Nodes" and "Edges" in the - extracted cluster -
-
- -
-
-

- filter_by_date(cutoff, - date_file, start_date, node, count_newly_add) -

-
-
-
- Check if the date attribute of a node falls within a - pre-specified range -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - + - - - - - +
+ Represents an HIV transmission network with annotations +
+ + + + + + + + + +
Parameters:
+ + +
NameTypeDescription
cutoff
date_file
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
json + + +Object + + + + A JSON object containing the network data.
button_bar_ui + + +HTMLElement + + + + A UI element for interacting with the network.
cluster_attributes + + +Object + + + + Attributes related to clusters within the network.
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
- - start_date - - - - - - node - - - - - - - - count_newly_add - - - - - [bool]; if true, then a "new node" attribute - overrides date checks, so all new (compared to the - previous network) nodes pass the check - - - - - -
-
- -
-
-

- generateClusterOfInterestID() -

-
-
-
- generate the name for a cluster of interest -
- -
-
- -
-
-

- get_ui_element_selector_by_role(role:, nested:) -

-
-
-
- retrive the DOM ID for an element given its - data-hivtrace-ui-role -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
role: - data-hivtrace-ui-role -
nested: - true if this is being called from a secondary - network or element (dialog, cluster view etc), which - does not have primary button_ui elements -
- -
-
- -
-
-

- initialize_ui_ux_elements() -

-
-
-
initialize UI/UX elements
- -
-
- -
-
-

- inject_attribute_description() -

-
-
-
- add an attribute description Given an attribute definition - (see comments elsewhere), and a key to associate it with - do -
- -
-
- -
-
-

- load_priority_sets(url, is_writeable) -

-
-
-
- read and process JSON files defining COI -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
url - [string]: load the data from here -
is_writeable - [string]: if "writeable", changes to COI lists will - be pushed back to the server This needs to be called - AFTER the clusters/subclusters have been annotated -
- -
-
- -
-
-

- map_ids_to_objects() -

-
-
-
- create a map between node IDs and node objects -
- -
-
- -
-
-

- parse_dates(value) -

-
-
-
parse a date record
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
value - (date object or string) -
- -
- -
Returns:
- -
date object
-
- -
-
-

- populate_predefined_attribute(computed, key) -

-
-
-
- populate_predefined_attribute Given an attribute - definition (see comments elsewhere), and a key to - associate it with do 0. Inject the definition of the - attribute into the network dictionary 1. Compute the value - of the attribute for all nodes 2. Compute unique values -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
computed - (dict) : attribute definition -
key - (string) : the key to associate with the attribute -
- -
-
- -
-
-

- priority_group_node_record(node_id, date, kind) -

-
-
-
Generate a CoI node record
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
node_id - [string] : node name, -
date - (optional) : creation date -
kind - (optional) : node creation mode -
- -
-
- -
-
-

- priority_groups_automatic() -

-
-
-
- filter the list of CoI to return those which have been - created by the system -
- -
-
- -
-
-

- priority_groups_compute_node_membership() -

-
-
-
- Compute which CoI do various nodes belong to, and define - additional attributes for each node -
- -
-
- -
-
-

- priority_groups_expanded() -

-
-
-
- filter the list of CoI to return those which have been - automatically expanded -
- -
-
- -
-
-

- priority_groups_pending() -

-
-
-
- filter the list of CoI to return those which have not been - reviewed/validated -
- -
-
- -
-
-

- priority_groups_validate(groups, auto_extend) -

-
-
-
validate the list of CoI
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
groups - array - - is a list of CoI name: unique string description: - string, nodes: { { 'id' : node id, 'added' : date, - 'kind' : _cdcPrioritySetNodeKind } }, created: date, - kind: kGlobals.CDCCOIKind, tracking: - kGlobals.CDCCOITrackingOptions createdBy : - kGlobals.CDCCOICreatedBySystem,kGlobals.CDCCOICreatedManually -
auto_extend - bool - - : if true, automatically expand existing CoI -
- -
-
- -
-
-

- <static> inject_attribute_node_value_by_id(node, id, value) -

-
-
-
- Add an attribute value to the node object -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
node[object] : node,
id - [string] : attribute id -
value: attribute value
- -
-
- -
-
-

- <static> is_edge_injected() -

-
-
-
- Is this node NOT genetic, i.e. added to the network via - social or other means -
- -
-
- -
-
-

- <static> is_new_node() -

-
-
-
- does the node have "new node" attribute -
- -
-
-
-
-
-
-
- -
- -
- -
-
-
- - - - - - - - - - - - - - - - - + + + + + + + + + + + + + +

Members

+ + + +

_calc_country_nodes

+ + + + +
+ this is a function which calculates country node centers + for the (experimental) option of rendering networks with + topo maps +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

auto_expand_pg_handler

+ + + + +
+ Grow a CoI defined in @pg based on its growth mode +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

charge_correction

+ + + + +
+ d3 layout option setting +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

check_for_time_series

+ + + + +
+ Generate a function callback for attribute time series data +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

cluster_filtering_functions

+ + + + +
+ filters which control which clusters get rendered +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

colorizer

+ + + + +
+ default node colorizer +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

defined_priority_groups

+ + + + +
+ the list of defined clusters of interest, + format as follows (SLKP, 20240715: may need updating) + { + 'name' : 'unique name', + 'nodes' : [ + { + 'node_id' : text, + 'added' : date, + 'kind' : text + }], + 'created' : date, + 'description' : 'text', + 'modified' : date, + 'kind' : 'text' + } +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

filter_by_size

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

filter_if_added

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

filter_singletons

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

filter_time_period

+ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

highlight_unsuppored_edges

+ + + + +
+ if there is computed support for network edges, use it to highlight + possible spurious edges +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

node_label_drag

+ + + + +
+ define a D3 behavior to make node labels draggable +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

node_shaper

+ + + + +
+ default node shaper +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_all_events

+ + + + +
+ generate a set of all unique temporal events (when new data were added to ANY CoI) + return a Set of date strings formatted with timeDateUtil.DateViewFormatSlider +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_compute_overlap

+ + + + +
+ compute the overlap between CoI +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_edit_set_description

+ + + + +
+ A function that updates the "freehand" description + of a specific CoI +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_export

+ + + + +
+ export CoI records for interactions with the external DB +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_export_nodes

+ + + + +
+ Export nodes that are members of CoI +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_export_sets

+ + + + +
+ Export CoI summary info +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_find_by_name

+ + + + +
+ lookup a CoI by name; null if not found +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_is_new_node

+ + + + +
+ returns true is the node was added by the system during CoI definition/expansion +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_remove_set

+ + + + +
+ Remove a CoI from the list of defined CoI +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

priority_groups_update_node_sets

+ + + + +
+ interact with the remote DB to send updates of CoI operations +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

using_time_filter

+ + + + +
+ time filter element for various displays +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

annotate_cluster_changes()

+ + + + + + +
+ annotate_cluster_changes + + If the network contains information about cluster changes (new/moved/deleted nodes, etc), + this function will annotate cluster objects (in place) with various attributes + "delta" : change in the size of the cluster + "flag" : a status flag to be used in the cluster display table + if set to 2 then TBD + if set to 3 then TBD +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

attribute_node_value_by_id(d:, id:, number:)

+ + + + + + +
+ Fetch the value of an attribute from the node +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
d: + + node object
id: + + [string] the attribute whose value should be fetched
number: + + [bool] if true, only return numerical values
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

cluster_display_filter()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

define_attribute_COI_membership(network, data:)

+ + + + + + +
+ define an attribute generator for subcluster membership attribute +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
network + + : the network / cluster object to ise
data: + + reference date to use
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ attribute definition +
+ + + + + + + + + + + + + + + +

define_attribute_binned_vl(field:, title:)

+ + + + + + +
+ define an attribute generator for binned viral loads +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
field: + + the node attribute field to use
title: + + display this title for the attribute
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ attribute definition dict +
+ + + + + + + + + + + + + + + +

define_attribute_dx_years(relative:, label:)

+ + + + + + +
+ define an attribute generator for dx year +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
relative: + + if T, compute dx date relative to the network date in years
label: + + use this label
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ attribute definition dict +
+ + + + + + + + + + + + + + + +

define_attribute_network_update()

+ + + + + + +
+ define an attribute generator for new network nodes/clusters +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ attribute definition dict +
+ + + + + + + + + + + + + + + +

define_attribute_vl_interpretaion()

+ + + + + + +
+ define an attribute generator for Viral load result interpretatio +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ attribute definition dict +
+ + + + + + + + + + + + + + + +

display_warning()

+ + + + + + +
+ display a warning string +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

extract_single_cluster(nodes, filter, no_clone)

+ + + + + + +
+ Extract the nodes and edges between them into a separate object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
nodes + + [array] the list of nodes to extract
filter + + [function, optional] (edge) -> bool filtering function for deciding which edges will be used to define clusters
no_clone + + [bool] if set to T, node objects are **not** shallow cloned in the return object
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ [dict] the object representing "Nodes" and "Edges" in the extracted cluster +
+ + + + + + + + + + + + + + + +

filter_by_date(cutoff, date_file, start_date, node, count_newly_add)

+ + + + + + +
+ Check if the date attribute of a node falls within a pre-specified range +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
cutoff + +
date_file + +
start_date + +
node + +
count_newly_add + + [bool]; if true, then a "new node" attribute overrides date checks, + so all new (compared to the previous network) nodes pass the check
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

generateClusterOfInterestID()

+ + + + + + +
+ generate the name for a cluster of interest +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

get_ui_element_selector_by_role(role:, nested:)

+ + + + + + +
+ retrive the DOM ID for an element given its data-hivtrace-ui-role +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
role: + + data-hivtrace-ui-role
nested: + + true if this is being called from a secondary network or element (dialog, cluster view etc), + which does not have primary button_ui elements
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

initialize_ui_ux_elements()

+ + + + + + +
+ initialize UI/UX elements +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

inject_attribute_description()

+ + + + + + +
+ add an attribute description + + Given an attribute definition (see comments elsewhere), and a key to associate it with + do +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

load_priority_sets(url, is_writeable)

+ + + + + + +
+ read and process JSON files defining COI +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
url + + [string]: load the data from here
is_writeable + + [string]: if "writeable", changes to COI lists will be pushed back to the server + + This needs to be called AFTER the clusters/subclusters have been annotated
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

map_ids_to_objects()

+ + + + + + +
+ create a map between node IDs and node objects +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

parse_dates(value)

+ + + + + + +
+ parse a date record +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
value + + (date object or string)
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + +
Returns:
+ + +
+ date object +
+ + + + + + + + + + + + + + + +

populate_predefined_attribute(computed, key)

+ + + + + + +
+ populate_predefined_attribute + + Given an attribute definition (see comments elsewhere), and a key to associate it with + do + + 0. Inject the definition of the attribute into the network dictionary + 1. Compute the value of the attribute for all nodes + 2. Compute unique values +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
computed + + (dict) : attribute definition
key + + (string) : the key to associate with the attribute
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

priority_group_node_record(node_id, date, kind)

+ + + + + + +
+ Generate a CoI node record +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
node_id + + [string] : node name,
date + + (optional) : creation date
kind + + (optional) : node creation mode
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

priority_groups_automatic()

+ + + + + + +
+ filter the list of CoI to return those which have been created by the system +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

priority_groups_compute_node_membership()

+ + + + + + +
+ Compute which CoI do various nodes belong to, and + define additional attributes for each node +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

priority_groups_expanded()

+ + + + + + +
+ filter the list of CoI to return those which have been automatically expanded +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

priority_groups_pending()

+ + + + + + +
+ filter the list of CoI to return those which have not been reviewed/validated +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

priority_groups_validate(groups, auto_extend)

+ + + + + + +
+ validate the list of CoI +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
groups + + +array + + + + is a list of CoI + name: unique string + description: string, + nodes: { + { + 'id' : node id, + 'added' : date, + 'kind' : _cdcPrioritySetNodeKind + } + }, + created: date, + kind: kGlobals.CDCCOIKind, + tracking: kGlobals.CDCCOITrackingOptions + createdBy : kGlobals.CDCCOICreatedBySystem,kGlobals.CDCCOICreatedManually
auto_extend + + +bool + + + + : if true, automatically expand existing CoI
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(static) inject_attribute_node_value_by_id(node, id, value)

+ + + + + + +
+ Add an attribute value to the node object +
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
node + + [object] : node,
id + + [string] : attribute id
value + + : attribute value
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(static) is_edge_injected()

+ + + + + + +
+ Is this node NOT genetic, i.e. added to the network via social or other means +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

(static) is_new_node()

+ + + + + + +
+ does the node have "new node" attribute +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/classes.list.html b/docs/classes.list.html deleted file mode 100644 index 9df5d01..0000000 --- a/docs/classes.list.html +++ /dev/null @@ -1,5566 +0,0 @@ - - - - - - - Documentation Classes - - - - - - - - - - -
-
-
-
-

Classes

-
-
-

-
- -
-
-
-
- -

Classes

- -
-
HIVTxNetwork
-
- -
- hivtrace_generate_svg_polygon -
-
-
- -

Members

- -
-
-
-

- <constant> CDCCOICanAutoExpand -

-
-
-
- CDCCOITrackingOptions: Available tracking options for - Clusters of Interest Used as drop-downs for COI editors - and automatic filling -
- -
-
- -
-
-

- <constant> CDCCOIConciseTrackingOptions -

-
-
-
- CDCCOITrackingOptionsDateFilter: A filtering option - applied to cluster nodes when computing COI membership - under different tracking options Specifies the maximum - number of MONTHS (relative to the reference data) that a - node age must fall into -
- -
-
- -
-
-

- <constant> CDCCOICreatedBySystem -

-
-
-
- CDCCOINodeKind: Available options for how a COI member was - identified Used as drop-downs for COI editors and - automatic filling -
- -
-
- -
-
-

- <constant> CDCCOICreatedManually -

-
-
-
- CDCCOICreatedBySystem: The value of "createdBy" in a COI - record when the COI was created by the system - (automatically) -
- -
-
- -
-
-

- <constant> CDCCOIKind -

-
-
-
- ShapeOrdering: The range to which values for - categorical/ordinal variables will be mapped using the - "Shape" dropdown -
- -
-
- -
-
-

- <constant> CDCCOIKindAutomaticCreation -

-
-
-
- CDCJurisdictionLowMorbidity: The set of low-morbidity - jurisdictions for Secure HIV TRACE -
- -
-
- -
-
-

- <constant> CDCCOINodeKind -

-
-
-
- CDCCOIConciseTrackingOptions: Available shorter tracking - options for Clusters of Interest Used for COI exports -
- -
-
- -
-
-

- <constant> CDCCOINodeKindDefault -

-
-
-
- CDCCOIKindAutomaticCreation: The kind of COI that is - automatically created by the system -
- -
-
- -
-
-

- <constant> CDCCOITrackingOptions -

-
-
-
- CDCCOIKind: Available creation options for Clusters of - Interest -
- -
-
- -
-
-

- <constant> CDCCOITrackingOptionsDateFilter -

-
-
-
- CDCCOITrackingOptionsFilter: A filtering option is applied - to cluster edges when computing COI membership under - different tracking options Selects edges of different - lengths (0.5% vs 1.5%) -
- -
-
- -
-
-

- <constant> CDCCOITrackingOptionsDefault -

-
-
-
- CDCCOICanAutoExpand: The types of tracking modes which are - subject to automatic growth tracking -
- -
-
- -
-
-

- <constant> CDCCOITrackingOptionsDistanceFilter -

-
-
-
- CDCCOITrackingOptionsNone: The "no-tracking" option - assigned for COI -
- -
-
- -
-
-

- <constant> CDCCOITrackingOptionsNone -

-
-
-
- CDCCOITrackingOptionsDefault: The default tracking option - assigned to COI -
- -
-
- -
-
-

- <constant> CDCJurisdictionCodes -

-
-
-
- CDCCOICreatedManually: The value of "createdBy" in a COI - record when the COI was created manually -
- -
-
- -
-
-

- <constant> CDCJurisdictionLowMorbidity -

-
-
-
- CDCJurisdictionCodes: Mapping from full names to - two-letter codes for various Secure HIV-TRACE - jurisdictions -
- -
-
- -
-
-

- <constant> CDCNPMember -

-
-
-
- CDCCOINodeKindDefault: The method of node identification - used by default (and it automatically created COI) -
- -
-
- -
-
-

- <constant> Categorical -

-
-
-
- CategoricalBase: The default set of colors used for - displaying colors of categorical attributes -
- -
-
- -
-
-

- <constant> DateFormats -

-
-
-
- Used to generate pop-over labels for node displays, and - COI views -
- -
-
- -
-
-

- <constant> DateUpperBoundYear -

-
-
-
- Used in many places where alpha-numerically sorted dates - are desired -
- -
-
- -
-
-

- <constant> DateViewFormat -

-
-
-
- Used to generate legend labels for date-valued attributes - for network displayes -
- -
-
- -
-
-

- <constant> DateViewFormatClusterCreate -

-
-
-
- Maximum year value (no future dates) -
- -
-
- -
-
-

- <constant> DateViewFormatShort -

-
-
-
- this is currently used to display node addition dates to - COI -
- -
-
- -
-
-

- <constant> DateViewFormatSlider -

-
-
-
- List of accepted time formats for attribute values -
- -
-
- -
-
-

- <constant> EdgeColorBase -

-
-
-
- MaximumValuesInCategories: Maximum # of distinct values - that can be rendered for categorical variables -
- -
-
- -
-
-

- <constant> MaximumValuesInCategories -

-
-
-
- Categorical: Expanded color range using darker shades - based on `CategoricalBase` -
- -
-
- -
-
-

- <constant> PresetColorSchemes -

-
-
-
- EdgeColorBase: The default color range for shading network - EDGES -
- -
-
- -
-
-

- <constant> PresetShapeSchemes -

-
-
-
- PresetColorSchemes: Predefined (hard-coded) color schemes - for specific attribute fields. Keys are exactly matched by - categorical variable names These should be updated if a - specific attribute needs to use a particular color scheme - for value rendering -
- -
-
- -
-
-

- <constant> SequentialColor -

-
-
-
- PresetShapeSchemes: Predefined (hard-coded) shape schemes - for specific attribute fields. Keys are exactly matched by - categorical variable names These should be updated if a - specific attribute needs to use a particular shape scheme - for value rendering -
- -
-
- -
-
-

- <constant> ShapeOrdering -

-
-
-
- SequentialColor: The default color ranges for a fixed - number of values -
- -
-
- -
-
-

- <constant> SubclusterSeparator -

-
-
-
- network: Attributes and string constants for the network - object -
- -
-
- -
-
-

- cluster_time_scale -

-
-
-
- used as a part of auto-named COI, e.g. NC_202105_44.1 -
- -
-
- -
-
-

- d3 -

-
-
-
- A collection of table column definitions -
- -
-
- -
-
-

- d3 -

-
-
-
- Combined definitions and presets for various network - components. -
- -
-
- -
-
-

- d3 -

-
-
-
- Functions that help manipulate network JSON and perform - other utility operations -
- -
-
- -
-
-

- <constant> formats -

-
-
-
- missing: Values to use when displaying missing values -
- -
-
- -
-
-

- <constant> missing -

-
-
-
- CDCNPMember: The label for the auto-generated node - attribute (ever in NP CoI) -
- -
-
- -
-
-

- <constant> network -

-
-
-
- formats: Various formatters for numerical values -
- -
-
-
- -

Methods

- -
-
-
-

- _action_drop_down(self, pg) -

-
-
-
- Generates a dropdown menu for actions on a cluster of - interest (COI). -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self - Object - - The main network visualization object. -
pg - Object - - The cluster of interest data. -
- -
- -
Returns:
- -
- An array of dropdown menu options. -
- -
-
Type
-
- Array -
-
-
- -
-
-

- add_a_sortable_table(container, headers, content [, - overwrite] [, caption] [, priority_set_editor]) -

-
-
-
- Adds a sortable table to a D3 selection container. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
container - d3.selection - - The D3 selection representing the container element - for the table. -
headers - Array.<string> - - An array of strings representing the table headers. -
content - Array.<Object> - - An array of objects representing the table content. - Each object should have properties that map to table - cells. -
overwrite - boolean - <optional>
- An optional flag indicating whether to overwrite any - existing table content (default: false). -
caption - string - <optional>
- An optional caption for the table. -
priority_set_editor - function - <optional>
- An optional function used to customize cell - formatting based on priority sets. The function - should accept four arguments: - `d`: The data object - for the current cell. - `i`: The index of the - current cell within its row. - `cell`: The D3 - selection of the current cell element (a ` - - ` element). - `priority_set_editor`: The - `priority_set_editor` function passed to - `add_a_sortable_table`. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- b64toBlob(b64, - onsuccess [, onerror]) -

-
-
-
- Converts a base64-encoded string to a Blob object. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
b64 - string - - The base64-encoded string. -
onsuccess - function - - A callback function to be called when the conversion - is successful. -
onerror - function - <optional>
- An optional callback function to be called if an - error occurs. -
- -
-
- -
-
-

- collapseLargeCategories(nodes, schema) -

-
-
-
- Collapses rare categories to "Other" category if there are - >CATEGORY_UNIQUE_VALUE_LIMIT categories -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
nodes - Array.<Object> - - An array of node objects, each containing patient - attributes. -
schema - Object - - An object defining the schema for the patient - attributes. -
- -
- -
Returns:
- -
- True if any categories were collapsed, false otherwise. -
- -
-
Type
-
- boolean -
-
-
- -
-
-

- colorPickerInput(id, - colorizer) -

-
-
-
- Creates a color picker input element with a label. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
id - string - - The ID for the color picker input element. -
colorizer - function - - A colorizer function that maps values to colors. -
- -
- -
Returns:
- -
- The HTML markup for the color picker input element with - the label. -
- -
-
Type
-
- string -
-
-
- -
-
-

- colorPickerInputContinuous(id, color) -

-
-
-
- Creates a color picker input element with a label for - continuous values. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
id - string - - The ID for the color picker input element. -
color - string - - The initial color value for the color picker. -
- -
- -
Returns:
- -
- The HTML markup for the color picker input element with - the label. -
- -
-
Type
-
- string -
-
-
- -
-
-

- colorStops(id, number) -

-
-
-
- Creates a color stop input element with a label. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
id - string - - The ID for the color stop input element. -
number - number - - The initial value for the color stop. -
- -
- -
Returns:
- -
- The HTML markup for the color stop input element with the - label. -
- -
-
Type
-
- string -
-
-
- -
-
-

- copyToClipboard(text) -

-
-
-
- Copies the given text to the clipboard. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
text - string - - The text to be copied. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- datamonkey_describe_vector(vector [, as_list]) -

-
-
-
- Calculates descriptive statistics for a numerical vector. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
vector - Array.<number> - - An array of numbers representing the data. -
as_list - boolean - <optional>
- An optional flag indicating whether to return the - statistics as a formatted string. -
- -
- -
Returns:
- -
- - If `as_list` is false, returns an object with the - following properties: - `min`: The minimum value. - `max`: - The maximum value. - `median`: The median value. - `Q1`: - The first quartile. - `Q3`: The third quartile. - `mean`: - The mean value. - If `as_list` is true, returns a - formatted string representing the statistics. -
- -
-
Type
-
- Object - | - - string -
-
-
- -
-
-

- datamonkey_export_csv_button(data [, name]) -

-
-
-
- Creates a downloadable CSV file for the provided data and - adds a button to trigger the download. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
data - Array.<Object> - - An array of objects representing the data to be - exported. -
name - string - <optional>
- An optional name for the exported CSV file. If not - provided, defaults to "export.csv". -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- datamonkey_export_handler(data [, filename] [, mimeType]) -

-
-
-
- Handles exporting data to a file based on browser - capabilities. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
data - string - - The data to be exported. -
filename - string - <optional>
- The desired filename for the downloaded file. - Defaults to "download.tsv". -
mimeType - string - <optional>
- The MIME type of the data. Defaults to "text/plain" - if not provided. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- datamonkey_export_json_button(data [, title]) -

-
-
-
- Creates a downloadable JSON file for the provided data and - adds a button to trigger the download. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
data - Object - | - - Array.<Object> - - The data to be exported, either a single object or - an array of objects. -
title - string - <optional>
- An optional title for the exported JSON file. If not - provided, defaults to "export". -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- datamonkey_save_image(type, container) -

-
-
-
- Saves the contents of an SVG element as an image file. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
type - string - - The desired image format (either "svg" or "png"). -
container - jQuery - | - - HTMLElement - - A jQuery selector or element reference containing - the SVG element. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- draw_priority_set_table(self, container, priority_groups) -

-
-
-
- Draws a table of priority sets (clusters of interest). -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self - Object - - The main network visualization object. -
container - HTMLElement - - The HTML element where the table will be displayed - (optional). -
priority_groups - Array - - An array of objects representing the priority sets - (optional). -
- -
-
- -
-
-

- edge_typer(e, - edge_types, T) -

-
-
-
- Determines the type of an edge based on its length and - predefined edge types. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
e - Object - - The edge object to be classified. -
edge_types - Array.<string> - - An array of two edge types. The first type is used - for edges shorter than or equal to `T`, and the - second type is used for edges longer than `T`. -
T - number - - The threshold value for edge length classification. -
- -
- -
Returns:
- -
- The edge type corresponding to the edge's length. -
- -
-
Type
-
- string -
-
-
- -
-
-

- ensure_node_attributes_exist() -

-
-
-
- ensure_node_attributes_exist Iterate over nodes in the - network. If a node does not have an array of attributes or - data dictionary records, create an empty one. This makes - error checking less complex downstream. -
- -
-
- -
-
-

- exportColorScheme(uniqValues, colorizer) -

-
-
-
- Exports a color scheme based on unique values and a - colorizer function. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
uniqValues - Object - - An object containing unique values for each - attribute, as returned by `getUniqueValues`. -
colorizer - function - - A colorizer function that maps values to colors. -
- -
- -
Returns:
- -
- An object where the keys are unique values and the values - are the corresponding colors. -
- -
-
Type
-
- Object -
-
-
- -
-
-

- filter_parse(filter_value) -

-
-
-
- Parses a filter string into an array of filter objects. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
filter_value - string - - The filter string to be parsed. -
- -
- -
Returns:
- -
- An array of filter objects, each with a `type` property - and a corresponding `value` property. The `type` can be - "re" for regular expression, "date" for date range, or - "distance" for numerical comparison. -
- -
-
Type
-
- Array.<Object> -
-
-
- -
-
-

- filter_table(element) -

-
-
-
- Filters a D3 table based on user-defined filters in column - headers. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
element - d3.selection - | - - HTMLElement - - The D3 selection or HTML element representing a - table header cell that triggered the filter -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- filter_table_by_column_handler(datum, conditions) -

-
-
-
- Filters a table based on specified conditions applied to a - column. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
datum - * - - The data object representing a row in the table. -
conditions - Array.<Object> - - An array of condition objects, each with the - following properties: - `type`: The type of - condition ("re" for regular expression, "date" for - date range, "distance" for numerical comparison). - - `value`: The value or range for the condition: - For - "re": A regular expression object. - For "date": An - array of two Date objects representing the start and - end dates. - For "distance": A number representing - the threshold value. - `greater_than` (optional): A - boolean indicating whether to use greater-than - comparison for "distance" conditions. -
- -
- -
Returns:
- -
- True if the row matches at least one condition, false - otherwise. -
- -
-
Type
-
- boolean -
-
-
- -
-
-

- format_a_cell(data, - index, item [, priority_set_editor]) -

-
-
-
- Formats a cell in a table based on provided data. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
data - Object - - The data object representing the table cell. It - should have properties like: - `value`: The cell - value. - `format` (optional): A function used to - format the value. - `html` (optional): A flag - indicating whether the value should be set as HTML. - - `callback` (optional): A function used to - customize cell content and behavior. - `filter` - (optional): A flag indicating whether to enable - filtering for the column. - `column_id` (optional): - The index of the column (used for filtering). - - `sort` (optional): A flag indicating whether to - enable sorting for the column. - `presort` - (optional): A string ("asc" or "desc") for initial - sort direction. - `actions` (optional): An array of - button configurations for cell actions. - `help` - (optional): A string describing the cell content - (used as a tooltip). -
index - number - - The index of the cell within its row. -
item - d3.selection - - The D3 selection of the table cell element (a ` - ` element).
priority_set_editor - function - <optional>
- An optional function used for priority set - functionality (internal). -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- getUniqueValues(nodes, - schema) -

-
-
-
- Retrieves unique values for each attribute in a given - dataset. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
nodes - Array.<Object> - - An array of node objects, each containing patient - attributes. -
schema - Object - - An object defining the schema for the patient - attributes. -
- -
- -
Returns:
- -
- An object where each key represents an attribute name and - the corresponding value is an array of unique values for - that attribute. -
- -
-
Type
-
- Object -
-
-
- -
-
-

- get_editor() -

-
-
-
- Gets the current priority set editor object. -
- -
- -
Returns:
- -
- The priority set editor object, or null if not open. -
- -
-
Type
-
- Object -
-
-
- -
-
-

- get_ui_element_selector_by_role(role) -

-
-
-
- Retrieves a CSS selector for UI elements based on their - `data-hivtrace-ui-role` attribute. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
role - string - - The value of the `data-hivtrace-ui-role` attribute. -
- -
- -
Returns:
- -
- A CSS selector string targeting elements with the - specified role. -
- -
-
Type
-
- string -
-
-
- -
-
-

- handle_cluster_click(self:, cluster, release) -

-
-
-
- handle_cluster_click Handle contextual menus for clusters - and cluster drag -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self:network object
cluster - [optional]: the cluster object to act on -
release - [optional]: the cluster object to release the - "fixed" flag from -
- -
-
- -
-
-

- handle_inline_confirm(this_button, generator, text, - action, disabled) -

-
-
-
- Handles inline confirmation popovers. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
this_button - HTMLElement - - The button element that triggers the popover. -
generator - function - - A function that generates the HTML content for the - popover body. -
text - string - - The initial text to display in the popover's text - area (optional). -
action - function - - A callback function to be executed when the user - confirms the action. Takes the value from the text - area as input. -
disabled - boolean - - A flag indicating if the text area should be - disabled (optional). -
- -
-
- -
-
-

- hiv_trace_export_table_to_text(parent_id, table_id, csv - [, file_name_placeholder]) -

-
-
-
- This function creates a download button that, when - clicked, exports the data from the specified table as - either a CSV or TSV file. It uses D3 for DOM manipulation - and assumes the existence of a `helpers` object with - `table_to_text` and `export_handler` methods. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
parent_id - string - - The selector for the parent element where the button - will be appended. -
table_id - string - - The selector for the table to be exported. -
csv - boolean - - If true, exports as CSV; if false, exports as TSV. -
- file_name_placeholder - - string - <optional>
- The base name for the exported file. If not - provided, it defaults to the table_id without the - first character. -
- -
- -
Requires:
-
    -
  • module:d3
  • - -
  • module:helpers
  • -
- -
Returns:
- -
- The D3 selection of the created button. -
- -
-
Type
-
- d3.Selection -
-
- -
Example
- -
-hiv_trace_export_table_to_text("#parent-div", "#data-table", true, "export-data");
-
- -
-
-

- hivtraceClusterGraphSummary() -

-
-
-
- ## hivtraceClusterGraphSummary Function Creates and - populates a summary table for an HIV trace cluster graph. - #### Parameters `graph`: The input graph data. `tag`: The - HTML element to append the summary table to. `not_CDC`: A - flag indicating whether to include CDC-specific statistics - or not (optional). #### Returns None -
- -
-
- -
-
-

- hivtrace_cluster_depthwise_traversal(nodes, edges [, - edge_filter] [, save_edges] [, seed_nodes] [, white_list]) -

-
-
-
- Performs a depth-wise traversal on a cluster of nodes, - considering edges and optional filters. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
nodes - Array.<Object> - - An array of node objects. Each node should have an - `id` property. -
edges - Array.<Object> - - An array of edge objects. Each edge should have - `source` and `target` properties referencing node - IDs. -
edge_filter - function - <optional>
- An optional filtering function applied to edges - before traversal. The function should accept an edge - object and return a boolean indicating whether to - include the edge. -
save_edges - function - <optional>
- An optional function used to store traversed edges. - It should be called with an array where each element - represents the edges within a cluster. -
seed_nodes - Array.<Object> - <optional>
- An optional array of node objects to use as starting - points for traversal. If not provided, all nodes - will be considered. -
white_list - Set - <optional>
- An optional set of node IDs restricting traversal to - nodes within the set. -
- -
- -
Throws:
- -
-
-
- If an edge references non-existent nodes. -
-
-
-
-
Type
-
- Error -
-
-
-
- -
Returns:
- -
- An array of clusters, where each cluster is an array of - node objects. -
- -
-
Type
-
- Array.<Array.<Object>> -
-
-
- -
-
-

- hivtrace_coi_timeseries(cluster, element [, plot_width]) -

-
-
-
- This function creates a detailed time series visualization - for a cluster of interest (COI). It displays nodes and - events over time, with interactive features for - highlighting and displaying additional information. The - visualization includes: - A time axis - Lines representing - nodes - Circles representing events - Interactive - highlighting and tooltips - Time range boxes for context -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDefaultDescription
cluster - Object - - The cluster object containing node and event - information. -
- Properties -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
node_info - Object - - Information about nodes in the cluster. -
event_info - Object - - Information about events in the cluster. -
-
element - d3.Selection - - The D3 selection of the element where the - visualization will be rendered. -
plot_width - number - <optional>
1000 - The width of the plot in pixels. -
- -
- -
Requires:
-
    -
  • module:d3
  • - -
  • module:lodash
  • -
- -
Example
- -
-const cluster = {
-  node_info: {...},
-  event_info: {...}
-};
-const element = d3.select("#visualization-container");
-hivtrace_coi_timeseries(cluster, element, 1200);
-
- -
-
-

- hivtrace_compute_node_degrees(obj) -

-
-
-
- This function modifies the input object by adding a - 'degree' property to each node. The degree of a node is - the number of edges connected to it. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
obj - Object - - The graph object containing nodes and edges. -
- Properties -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
Nodes - Object - - An object representing the nodes of the - graph. -
Edges - Object - - An object representing the edges of the - graph. -
- Edges[].source - - string - - The source node of an edge. -
- Edges[].target - - string - - The target node of an edge. -
-
- -
- -
Example
- -
-const graph = {
-  Nodes: {
-    "1": {},
-    "2": {}
-  },
-  Edges: {
-    "e1": { source: "1", target: "2" }
-  }
-};
-hivtrace_compute_node_degrees(graph);
-// graph.Nodes["1"].degree === 1
-// graph.Nodes["2"].degree === 1
-
- -
-
-

- hivtrace_date_or_na_if_missing(date [, formatter]) -

-
-
-
- Formats a date using a specified formatter, or returns - "N/A" if the date is missing. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
date - Date - - The date object to be formatted. -
formatter - function - <optional>
- An optional formatter function used to format the - date. If not provided, `DateViewFormatExport` is - used. -
- -
- -
Returns:
- -
- The formatted date string, or "N/A" if the date is - missing. -
- -
-
Type
-
- string -
-
-
- -
-
-

- hivtrace_generate_svg_symbol(type) -

-
-
-
- Generates an SVG symbol based on the specified type. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
type - string - - The type of symbol to generate. Possible values are: - - "circle" - "cross" - "diamond" - "square" - - "triangle-down" - "triangle-up" - "triangle" - - "pentagon" - "hexagon" - "septagon" - "octagon" - - "ellipse" -
- -
- -
Requires:
-
    -
  • module:d3
  • - -
  • module:hivtrace_generate_svg_polygon
  • - -
  • module:hivtrace_generate_svg_ellipse
  • -
- -
Returns:
- -
- A D3 symbol generator or a custom polygon/ellipse - generator. - For "circle", "cross", "diamond", "square", - "triangle-down", "triangle-up": Returns a D3 symbol - generator of the specified type. - For "triangle", - "pentagon", "hexagon", "septagon", "octagon": Returns a - custom polygon generator with the specified number of - sides. - For "ellipse": Returns a custom ellipse - generator. - For any other input: Returns a D3 symbol - generator of type "circle" as default. -
- -
-
Type
-
- Object -
-
-
- -
-
-

- hivtrace_histogram(graph, histogram_tag, histogram_label) -

-
-
-
- Renders a histogram for a graph property and displays a - descriptive label. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
graph - Object - - An object containing graph data, including - distribution and fitted data for the property. -
histogram_tag - string - - The ID of the HTML element where the histogram will - be rendered. -
histogram_label - string - - The ID of the HTML element where the descriptive - label will be displayed. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- hivtrace_histogram_distances(graph, histogram_tag, - histogram_label) -

-
-
-
- Renders a histogram for edge lengths (genetic distances) - and displays a label. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
graph - Object - - An object containing graph data, including edges. -
histogram_tag - string - - The ID of the HTML element where the histogram will - be rendered. -
histogram_label - string - - The ID of the HTML element where the descriptive - label will be displayed. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- hivtrace_plot_cluster_dynamics(time_series, container, - x_title, y_title [, y_scale] [, bin_by] [, options]) -

-
-
-
- Generates a plot visualizing cluster dynamics over time. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
time_series - Array.<Object> - - An array of data points, each with a required `time` - property (a date object) and optional properties - representing attributes. -
container - d3.selection - - A D3 selection representing the container element - for the plot. -
x_title - string - - The title for the x-axis. -
y_title - string - - The title for the y-axis. -
y_scale - d3.scale - <optional>
- An optional D3 scale for the y-axis. If not - provided, a linear scale will be used. -
bin_by - function - <optional>
- An optional function used to bin data points into - time intervals. The function should accept a date - object and return an array with three elements: - - The bin label (e.g., "Q1 2023"). - The start date of - the bin. - The middle date of the bin (used for - x-axis positioning). If not provided, a default - function that bins by quarters is used. -
options - Object - <optional>
- An optional configuration object for the plot. - - `base_line`: (number, default: 20) The baseline - value for the y-axis. - `top`: (number, default: 40) - The top padding for the plot. - `right`: (number, - default: 30) The right padding for the plot. - - `bottom`: (number, default: 60) The bottom padding - for the plot. - `left`: (number, default: 100) The - left padding for the plot. - `font_size`: (number, - default: 18) The font size for labels and text - elements. - `rect_size`: (number, default: 22) The - size of rectangles used in the legend. - `width`: - (number, default: 1024) The width of the plot - container. - `height`: (number, default: 600) The - height of the plot container. - `barchart`: - (boolean, default: false) If true, the plot will be - displayed as a bar chart. - `skip_cumulative`: - (boolean, default: false) If true, the cumulative - area will not be displayed. - `x-tick-format`: - (Function) An optional function for formatting - x-axis tick labels. - `prefix`: (string) An optional - prefix to add to attribute names displayed in the - legend. - `colorizer`: (Object) An optional - colorizer object for attributes. Keys should be - attribute names, and values should be D3 scales used - for coloring lines/bars. - `drag`: (Object) An - optional drag object for enabling dragging the plot. -
- -
- -
Throws:
- -
-
-
- If no data points are provided. -
-
-
-
-
Type
-
- Error -
-
-
-
-
- -
-
-

- hivtrace_render_histogram(counts [, fit], w, h, id) -

-
-
-
- Renders a histogram for discrete data using D3.js. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
counts - Array.<number> - - An array of counts for each category. -
fit - Array.<number> - <optional>
- (optional) - An array of fitted values for each - category. -
w - number - - The width of the plot area. -
h - number - - The height of the plot area. -
id - string - - The ID of the HTML element where the histogram will - be rendered. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- hivtrace_render_histogram_continuous(data, w, h, id) -

-
-
-
- Renders a histogram for continuous data using D3.js. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - Array.<number> - - An array of numerical values. -
w - number - - The width of the plot area. -
h - number - - The height of the plot area. -
id - string - - The ID of the HTML element where the histogram will - be rendered. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- hivtrace_render_scatterplot(points, w, h, id, labels [, - dates]) -

-
-
-
- Renders a scatter plot using D3.js. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDefaultDescription
points - Array.<Object> - - An array of data points, each with x and y - coordinates (and optionally a title). -
w - number - - The width of the plot area. -
h - number - - The height of the plot area. -
id - string - - The ID of the HTML element where the plot will be - rendered. -
labels - Object - - An object containing labels for the x and y axes. -
dates - boolean - <optional>
false - A flag indicating whether the x-axis should - represent dates. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- init(self) -

-
-
-
- Initializes the component, setting up event listeners and - UI elements. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self - Object - - The component object itself. -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- n_months_ago(reference_date, months) -

-
-
-
- Calculates a date that is `months` months ago from a given - reference date. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
reference_date - Date - - The reference date from which to calculate the past - date. -
months - number - - The number of months to go back. -
- -
- -
Returns:
- -
- A new Date object representing the date `months` months - ago from the reference date. -
- -
-
Type
-
- Date -
-
-
- -
-
-

- normalize_node_attributes() -

-
-
-
- normalize_node_attributes Iterate over node attributes, - lower case all the keys for mapping. If attributes are - found that are not in the data dictionary, attempt to map - them using "labels". -
- -
-
- -
-
-

- open_editor(self, - node_set, name, description, cluster_kind, kind_options, - validation_mode, existing_set, cluster_tracking, - created_by) -

-
-
-
- Opens a priority node set editor. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self - Object - - The main network visualization object. -
node_set - Array - - An existing priority node set (optional). -
name - string - - Name of the priority node set (optional for new - sets). -
description - string - - Description of the priority node set (optional). -
cluster_kind - string - - The method used to identify the cluster (optional). -
kind_options - Array - - Available options for cluster identification - methods. -
validation_mode - string - - Indicates the mode (create, validate, revise). -
existing_set - Object - - Reference to the existing priority node set (for - revisions). -
cluster_tracking - string - - Method for tracking cluster growth (optional). -
created_by - string - - Who created the node set (system or manual). -
- -
-
- -
-
-

- priority_groups_add_set(self, nodeset, update_table, - not_validated, prior_name, op_code) -

-
-
-
- Adds a new priority set to the network visualization. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self - Object - - The main network visualization object. -
nodeset - Object - - The object representing the new priority set. -
update_table - boolean - - Flag indicating whether to update the priority set - table. (optional) -
not_validated - boolean - - Flag indicating whether to perform validation before - adding. (optional) -
prior_name - string - - Optional name of an existing priority set to - replace. -
op_code - string - - Optional operation code (defaults to "insert"). -
- -
- -
Returns:
- -
- True if the set was added successfully, false otherwise. -
- -
-
Type
-
- boolean -
-
-
- -
-
-

- priority_groups_check_name(defined_priority_groups, - string [, prior_name]) -

-
-
-
- A valid name must: - Have a length between 1 and 35 - characters. - Not be a duplicate among existing priority - groups (excluding itself if editing). -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
- defined_priority_groups - - Array.<Object> - - An array of existing priority group objects. -
string - string - - The name to be validated. -
prior_name - string - <optional>
- (optional) - The previous name of the priority group - (used for edit case). -
- -
- -
Returns:
- -
- True if the name is valid, false otherwise. -
- -
-
Type
-
- boolean -
-
-
- -
-
-

- priority_set_inject_node_attibutes(self, nodes, - node_attributes) -

-
-
-
- Injects priority set related attributes into network - nodes. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self - Object - - The main network visualization object. -
nodes - Array - - Array of network nodes. -
node_attributes - Array - - Array of priority set attributes for specific nodes - (identified by name). -
- -
-
- -
-
-

- priority_set_view(self, priority_set, options) -

-
-
-
- Creates a subcluster view for a specific priority set. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self - Object - - The main network visualization object. -
priority_set - Object - - The priority set object. -
options - Object - - Optional configuration options for the view. -
- -
-
- -
-
-

- random_id( [alphabet] - [, length]) -

-
-
-
- Generates a random ID string using a specified alphabet - and length. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
alphabet - Array.<string> - <optional>
- An optional array of characters to use in the ID. If - not provided, a default alphabet of letters "a" to - "g" is used. -
length - number - <optional>
- An optional length for the ID. If not provided, a - default length of 32 is used. -
- -
- -
Returns:
- -
- A randomly generated ID string. -
- -
-
Type
-
- string -
-
-
- -
-
-

- secure_hiv_trace_subcluster_columns(self) -

-
-
-
- Defines secure column definitions for HIV Trace subcluster - data. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
self - Object - - (optional) - The object containing context for - calculations (presumably the component using this - function). -
- -
- -
Returns:
- -
- An array of column definition objects. Each object has the - following properties: - `description`: - `value`: (string) - The human-readable name of the column. - `sort`: - (function) A function used to sort the column data. - - `presort` (string, optional): The default sort direction - ("asc" or "desc"). - `help`: (string) Help text displayed - when hovering over the column header. - `generator`: - (function) A function that generates the value and actions - for each cluster based on the provided cluster object. - - The generator function receives the cluster object as an - argument. - It should return an object with the following - properties: - `html`: (boolean) Whether the column value - should be rendered as HTML. - `value`: (array|string) The - actual data for the column. - `volatile`: (boolean, - optional) Whether the value needs to be recalculated - frequently. - `format`: (function, optional) A function - used to format the column value for display. - `actions`: - (function, optional) A function used to generate actions - for the column. - The actions function receives two - arguments: - `item`: (object) The current cluster object. - - `value`: (array|string) The value of the column for the - current cluster. - It should return an array of action - objects, each with the following properties: - `icon`: - (string) The icon class name to display for the action. - - `action`: (function) The function executed when the action - is clicked. - The action function receives two arguments: - - `button`: (jQuery object) The button element - representing the action. - `v`: (array|string) The value - of the column for the current cluster. - `help`: (string) - The help text displayed when hovering over the action - icon. -
- -
-
Type
-
- Array.<Object> -
-
-
- -
-
-

- sort_table_by_column(element, datum) -

-
-
-
- Sorts a D3 table based on the clicked column header. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
element - d3.selection - | - - HTMLElement - - HTML element representing the clicked column header. -
datum - * - - (optional) - The data object associated with the - table (used internally). -
- -
- -
Returns:
- -
-
Type
-
- void -
-
-
- -
-
-

- sort_table_toggle_icon(element [, value]) -

-
-
-
- Toggles the sort icon on a column header and returns a - sorting function. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeArgumentDescription
element - d3.selection - | - - HTMLElement - - The D3 selection or HTML element representing the - column header. -
value - string - <optional>
- (optional) - The desired sort direction ("asc", - "desc", or "unsorted"). -
- -
- -
Returns:
- -
- - If `value` is provided, returns nothing. - If `value` is - not provided, returns a sorting function (`d3.ascending` - or `d3.descending`) based on the current sort state. -
- -
-
Type
-
- function - | - - void -
-
-
- -
-
-

- table_get_cell_value(data) -

-
-
-
- Retrieves the value of a cell in a table data object. -
- -
Parameters:
- - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
data - Object - - The data object representing the table cell. It - should have a `value` property. -
- -
- -
Returns:
- -
- The value of the cell, or the result of calling - `data.value()` if it's a function. -
- -
-
Type
-
- * -
-
-
- -
-
-

- unpack_compact_json() -

-
-
-
- unpack_compact_json: If the input network JSON is in - compact form, i.e. instead of storing key : value it - stores key : integer index of value unique_values: list of - values convert it to key: value The operation is performed - in place on the `json` argument -
- -
-
-
-
-
-
-
- -
- -
- -
-
-
- - - - - - - - - - - - - - - - - - - diff --git a/docs/clusternetwork.js.html b/docs/clusternetwork.js.html new file mode 100644 index 0000000..087f354 --- /dev/null +++ b/docs/clusternetwork.js.html @@ -0,0 +1,7758 @@ + + + + + JSDoc: Source: clusternetwork.js + + + + + + + + + + +
+ +

Source: clusternetwork.js

+ + + + + + +
+
+
import * as d3 from "d3";
+import _ from "underscore";
+import jsConvert from "js-convert-case";
+import * as topojson from "topojson";
+import * as helpers from "./helpers.js";
+import * as colorPicker from "./colorPicker.js";
+import * as scatterPlot from "./scatterplot.js";
+import * as tables from "./tables.js";
+import * as timeDateUtil from "./timeDateUtil.js";
+import * as nodesTab from "./nodesTab.js";
+import * as clustersOfInterest from "./clustersOfInterest.js";
+import { hivtrace_cluster_depthwise_traversal } from "./misc";
+import * as misc from "./misc";
+import * as kGlobals from "./globals.js";
+import * as network from "./network.js";
+import * as HTX from "./hiv_tx_network.js";
+import * as columnDefinitions from "./column_definitions.js";
+
+var hivtrace_cluster_network_graph = function (
+  json,
+  network_container,
+  network_status_string,
+  network_warning_tag,
+  button_bar_ui,
+  attributes,
+  filter_edges_toggle,
+  clusters_table,
+  nodes_table,
+  parent_container,
+  options
+) {
+  // [REQ] json                        :          the JSON object containing network nodes, edges, and meta-information
+  // [REQ] network_container           :          the CSS selector of the DOM element where the SVG containing the network will be placed (e.g. '#element')
+  // [OPT] network_status_string       :          the CSS selector of the DOM element where the text describing the current state of the network is shown (e.g. '#element')
+  // [OPT] network_warning_tag         :          the CSS selector of the DOM element where the any warning messages would go (e.g. '#element')
+  // [OPT] button_bar_ui               :          the ID of the control bar which can contain the following elements (prefix = button_bar_ui value)
+  //                                                - [prefix]_cluster_operations_container : a drop-down for operations on clusters
+  //                                                - [prefix]_attributes :  a drop-down for operations on attributes
+  //                                                - [prefix]_filter : a text box used to search the graph
+  // [OPT] network_status_string       :          the CSS selector of the DOM element where the text describing the current state of the network is shown (e.g. '#element')
+  // [OPT] attributes                  :          A JSON object with mapped node attributes
+
+  // unpack compact JSON if needed
+
+  if (json.Settings && json.Settings.compact_json) {
+    network.unpack_compact_json(json);
+  }
+
+  // if schema is not set, set to an empty dictionary
+  if (!json[kGlobals.network.GraphAttrbuteID]) {
+    json[kGlobals.network.GraphAttrbuteID] = {};
+  }
+
+  // Make attributes case-insensitive by LowerCasing all keys in schema
+  json[kGlobals.network.GraphAttrbuteID] = Object.fromEntries(
+    Object.entries(json[kGlobals.network.GraphAttrbuteID]).map(([k, v]) => [
+      k.toLowerCase(),
+      v,
+    ])
+  );
+
+  // Attempt Translations
+  $("#filter_input")
+    .val("")
+    .attr("placeholder", __("network_tab")["text_in_attributes"]);
+  $("#show_as").html(__("attributes_tab")["show_as"]);
+
+  network.normalize_node_attributes(json);
+  network.ensure_node_attributes_exist(json);
+
+  /** SLKP 20190902: somehow some of our networks have malformed edges! This will remove them */
+  json.Edges = _.filter(json.Edges, (e) => "source" in e && "target" in e);
+
+  var self = new HTX.HIVTxNetwork(json, button_bar_ui);
+
+  self._is_CDC_ = options && options["no_cdc"] ? false : true;
+  self._is_seguro = network.check_network_option(
+    options,
+    "seguro",
+    false,
+    true
+  );
+  self._is_CDC_executive_mode = network.check_network_option(
+    options,
+    "cdc-executive-mode",
+    false
+  );
+
+  self.uniqValues = helpers.getUniqueValues(
+    json.Nodes,
+    json[kGlobals.network.GraphAttrbuteID]
+  );
+  self.uniqs = _.mapObject(self.uniqValues, (d) => d.length);
+
+  self.schema = json[kGlobals.network.GraphAttrbuteID];
+  // set initial color schemes
+  self.networkColorScheme = kGlobals.PresetColorSchemes;
+  self.networkShapeScheme = kGlobals.PresetShapeSchemes;
+
+  self.ww = network.check_network_option(
+    options,
+    "width",
+    d3.select(parent_container).property("clientWidth")
+  );
+
+  self.margin = {
+    top: 20,
+    right: 10,
+    bottom: 30,
+    left: 10,
+  };
+  self.width = self.ww - self.margin.left - self.margin.right;
+  self.height = (self.width * 9) / 16;
+
+  self.container = network_container;
+  self.nodes = [];
+  self.edges = [];
+  self.clusters = [];
+  self.cluster_sizes = [];
+  self.cluster_mapping = {};
+  self.percent_format = kGlobals.formats.PercentFormat;
+  self.missing = kGlobals.missing.label;
+  self.cluster_attributes = json["Cluster description"] || null;
+  self.precomputed_subclusters = json["Subclusters"] || null;
+  self.network_warning_tag = network_warning_tag;
+
+  self.annotate_cluster_changes();
+
+  self.filter_edges = true;
+  self.hide_hxb2 = false;
+  self.cluster_table = d3.select(clusters_table);
+
+  if (self._is_CDC_) {
+    self.priority_set_table = network.check_network_option(
+      options,
+      "priority-table"
+    );
+    self.priority_set_table_write = network.check_network_option(
+      options,
+      "priority-table-writeback"
+    );
+    if (self.priority_set_table)
+      self.priority_set_table = d3.select(self.priority_set_table);
+  } else {
+    self.priority_set_table = null;
+    self.priority_set_table_write = null;
+  }
+
+  self.needs_an_update = false;
+  self.hide_unselected = false;
+  self.show_percent_in_pairwise_table = false;
+
+  self.priority_set_table_writeable = true;
+
+  /** if there's a function passed as "init_code", run it now */
+
+  if (options && _.isFunction(options["init_code"])) {
+    options["init_code"].call(null, self, options);
+  }
+
+  /** Whenever the code creates a DOM element, it will be done using this prefix 
+      to generate element IDs
+   */
+
+  self.dom_prefix = network.check_network_option(
+    options,
+    "prefix",
+    "hiv-trace"
+  );
+
+  /** Retrieve additional columns (as dict, see comments further down in the code)
+      for the "clusters" table
+  
+   */
+  self.extra_cluster_table_columns = network.check_network_option(
+    options,
+    "cluster-table-columns",
+    null
+  );
+
+  /** Not primary networks are individual cluster/subcluster views.
+      They don't interfere with the primary network object, and UI elements
+  
+   */
+
+  self.isPrimaryGraph = network.check_network_option(
+    options,
+    "secondary",
+    true,
+    false
+  );
+
+  self.parent_graph_object = network.check_network_option(
+    options,
+    "parent_graph",
+    null
+  );
+
+  /** set the TODAY date for the network*/
+
+  if (json.Settings && json.Settings.created) {
+    self.today = new Date(json.Settings.created);
+  } else {
+    self.today = network.check_network_option(
+      options,
+      "today",
+      timeDateUtil.getCurrentDate()
+    );
+  }
+
+  /** get the reference (creation) date for the network.
+      it's the same as "today" for primary networks,
+      but is inherited from parent networks for secondary graphs (e.g. cluster or subcluster views)
+  */
+
+  if (self._is_CDC_) {
+    // define various CDC settings
+
+    /** Do not automatically create CoI */
+    self._is_CDC_auto_mode = network.check_network_option(
+      options,
+      "cdc-no-auto-priority-set-mode",
+      true,
+      false
+    );
+
+    /** these are the default columns selected in the "nodes" table */
+
+    self.displayed_node_subset = network.check_network_option(
+      options,
+      "node-attributes",
+      [
+        tables._networkNodeIDField,
+        "sex_trans",
+        "race_cat",
+        "hiv_aids_dx_dt",
+        "cur_city_name",
+      ]
+    );
+
+    /** retrieve the target DOM ID for placing the "subcluster" table into */
+    self.subcluster_table = network.check_network_option(
+      options,
+      "subcluster-table",
+      null,
+      d3.select(options["subcluster-table"])
+    );
+
+    /** extra column definitions for the subcluster table */
+    self.extra_subcluster_table_columns = null;
+
+    // SLKP 20200727 issues
+
+    /** Secure HIV-TRACE specific settings */
+    self.CDC_data = {
+      jurisdiction: self
+        .lookup_option("jurisdiction", "unknown", options)
+        .toLowerCase()
+        .replace(/\s/g, ""),
+      timestamp: self.today,
+      "autocreate-priority-set-size": 5,
+    };
+
+    /** What jurisdiction are we using for Secure HIV-TRACE?
+        This determines the two-letter prefix for auto-naming Clusters of Interest
+        If none, use PG prefix
+     */
+
+    if (self.CDC_data.jurisdiction in kGlobals.CDCJurisdictionCodes) {
+      self.CDC_data["jurisdiction_code"] =
+        kGlobals.CDCJurisdictionCodes[self.CDC_data.jurisdiction].toUpperCase();
+    } else {
+      self.CDC_data["jurisdiction_code"] = "PG";
+    }
+
+    /** Check if this is a low morbidity jurisdiction */
+    if (
+      kGlobals.CDCJurisdictionLowMorbidity.has(self.CDC_data["jurisdiction"])
+    ) {
+      self.CDC_data["autocreate-priority-set-size"] = 3;
+    }
+
+    /** Populate column table definitions */
+    if (self.subcluster_table) {
+      self.extra_subcluster_table_columns =
+        columnDefinitions.secure_hiv_trace_subcluster_columns(self);
+    } else if (self.extra_cluster_table_columns) {
+      self.extra_cluster_table_columns =
+        self.extra_cluster_table_columns.concat(
+          columnDefinitions.secure_hiv_trace_subcluster_columns(self)
+        );
+    } else {
+      self.extra_cluster_table_columns =
+        columnDefinitions.secure_hiv_trace_subcluster_columns(self);
+    }
+  } // end self._is_CDC_
+
+  if (self._is_CDC_) {
+    self.extra_node_table_columns = null;
+  } else {
+    self.extra_node_table_columns = network.check_network_option(
+      options,
+      "node-table-columns"
+    );
+  }
+
+  /** default subcluster definition threshold */
+  self.subcluster_threshold = network.check_network_option(
+    options,
+    "subcluster-thershold",
+    0.005
+  );
+
+  clustersOfInterest.init(self);
+  nodesTab.init(d3.select(nodes_table));
+
+  self.countryCentersObject = network.check_network_option(
+    options,
+    "country-centers"
+  );
+  self.countryOutlines = network.check_network_option(
+    options,
+    "country-outlines"
+  );
+
+  if (self.countryCentersObject && self.countryOutlines) {
+    self._calc_country_nodes(options);
+    self.showing_on_map = options.showing_on_map;
+  } else {
+    self.showing_on_map = false;
+  }
+
+  /** this array contains fields that will be appended to node pop-overs in the network tab
+      they will precede all the fields that are shown based on selected labeling */
+  self._additional_node_pop_fields = [];
+
+  self.minimum_cluster_size = network.check_network_option(
+    options,
+    "minimum size",
+    5
+  );
+
+  timeDateUtil.init(options, self._is_CDC_, timeDateUtil._networkCDCDateField);
+
+  if (self._is_CDC_) {
+    self._additional_node_pop_fields.push(timeDateUtil._networkCDCDateField);
+  }
+
+  self.core_link_length = network.check_network_option(
+    options,
+    "core-link",
+    -1
+  );
+  self.additional_edge_styler = network.check_network_option(
+    options,
+    "edge-styler"
+  );
+
+  if (self.json.Notes) {
+    _.each(self.json.Notes, (s) => (self.warning_string += s + "<br>"));
+  }
+
+  /**
+    the true branch is taken if the network JSON contains "Cluster description"
+  */
+  if (self.cluster_attributes) {
+    self.warning_string += __("network_tab")["cluster_display_info"];
+    self.showing_diff = true;
+    self.cluster_filtering_functions["new"] = self.filter_if_added;
+  } else {
+    self.showing_diff = false;
+    if (
+      timeDateUtil.getClusterTimeScale() &&
+      "Cluster sizes" in self.json &&
+      self.json["Cluster sizes"].length > 250
+    ) {
+      self.using_time_filter = timeDateUtil.getCurrentDate();
+      self.warning_string += __("network_tab")["cluster_display_info"];
+      self.using_time_filter.setFullYear(
+        self.using_time_filter.getFullYear() - 1
+      );
+      self.cluster_filtering_functions["recent"] = self.filter_time_period;
+    }
+  }
+
+  self.initial_packed =
+    options && options["initial_layout"] === "tiled" ? false : true;
+
+  self._networkPredefinedAttributeTransforms = {
+    /** runtime computed node attributes, e.g. transforms of existing attributes */
+
+    binned_vl_recent_value: self.define_attribute_binned_vl(
+      "vl_recent_value",
+      "Most Recent Viral Load Category Binned"
+    ),
+
+    binned_vl_recent_value_adj: self.define_attribute_binned_vl(
+      "vl_recent_value_adj",
+      "Most Recent Viral Load Category Binned"
+    ),
+
+    vl_result_interpretation: self.define_attribute_vl_interpretaion(),
+
+    age_dx_normalized: self.define_attribute_age_dx(),
+
+    years_since_dx: self.define_attribute_dx_years(
+      true,
+      "Years since diagnosis"
+    ),
+
+    hiv_aids_dx_dt_year: self.define_attribute_dx_years(
+      false,
+      "Diagnosis Year"
+    ),
+  };
+
+  if (self.cluster_attributes) {
+    self._networkPredefinedAttributeTransforms["_newly_added"] =
+      self.define_attribute_network_update();
+  }
+
+  /** inject any attributes passed through "computed-attributes" */
+
+  if (options && options["computed-attributes"]) {
+    _.extend(
+      self._networkPredefinedAttributeTransforms,
+      options["computed-attributes"]
+    );
+  }
+
+  /*------------ Network layout code ---------------*/
+
+  self._get_node_country = function (node) {
+    var countryCodeAlpha2 = self.attribute_node_value_by_id(node, "country");
+    if (countryCodeAlpha2 === kGlobals.missing.label) {
+      countryCodeAlpha2 = self.attribute_node_value_by_id(node, "Country");
+    }
+    return countryCodeAlpha2;
+  };
+
+  self._draw_topomap = function (no_redraw) {
+    if (options && "showing_on_map" in options) {
+      var countries = topojson.feature(
+        self.countryOutlines,
+        self.countryOutlines.objects.countries
+      ).features;
+      var mapsvg = d3.select("#" + self.dom_prefix + "-network-svg");
+      var path = d3.geo.path().projection(self.mapProjection);
+      countries = mapsvg.selectAll(".country").data(countries);
+
+      countries.enter().append("path");
+      countries.exit().remove();
+
+      self.countries_in_cluster = {};
+
+      _.each(self.nodes, (node) => {
+        var countryCodeAlpha2 = self._get_node_country(node);
+        var countryCodeNumeric =
+          self.countryCentersObject[countryCodeAlpha2].countryCodeNumeric;
+        if (!(countryCodeNumeric in self.countries_in_cluster)) {
+          self.countries_in_cluster[countryCodeNumeric] = true;
+        }
+      });
+
+      countries
+        .attr("class", "country")
+        .attr("d", path)
+        .attr("stroke", "saddlebrown")
+        .attr("fill", (d) => {
+          if (d.id in self.countries_in_cluster) {
+            return "navajowhite";
+          }
+          return "bisque";
+        })
+        .attr("stroke-width", (d) => {
+          if (d.id in self.countries_in_cluster) {
+            return 1.5;
+          }
+          return 0.5;
+        });
+    }
+    return self;
+  };
+
+  self.open_exclusive_tab_close = function (
+    tab_element,
+    tab_content,
+    restore_to_tag
+  ) {
+    //console.log (restore_to_tag);
+    $(restore_to_tag).tab("show");
+    $("#" + tab_element).remove();
+    $("#" + tab_content).remove();
+  };
+
+  self.open_exclusive_tab_view = function (
+    cluster_id,
+    custom_filter,
+    custom_name,
+    additional_options,
+    include_injected_edges
+  ) {
+    var cluster = _.find(
+      self.clusters,
+      (c) => String(c.cluster_id) === String(cluster_id)
+    );
+
+    if (!cluster) {
+      return;
+    }
+
+    additional_options = additional_options || {};
+
+    additional_options["parent_graph"] = self;
+
+    var filtered_json = self.extract_single_cluster(
+      custom_filter
+        ? _.filter(self.json.Nodes, custom_filter)
+        : cluster.children,
+      null,
+      null,
+      null,
+      include_injected_edges
+    );
+
+    if (kGlobals.network.GraphAttrbuteID in json) {
+      filtered_json[kGlobals.network.GraphAttrbuteID] = {};
+      $.extend(
+        true,
+        filtered_json[kGlobals.network.GraphAttrbuteID],
+        json[kGlobals.network.GraphAttrbuteID]
+      );
+    }
+
+    var export_items = [];
+    if (!self._is_CDC_executive_mode) {
+      export_items.push([
+        "Export cluster to .CSV",
+        function (network) {
+          helpers.export_csv_button(
+            self._extract_attributes_for_nodes(
+              self._extract_nodes_by_id(cluster_id),
+              self._extract_exportable_attributes()
+            )
+          );
+        },
+      ]);
+    }
+
+    //self._check_for_time_series(export_items);
+
+    if ("extra_menu" in additional_options) {
+      _.each(export_items, (item) => {
+        additional_options["extra_menu"]["items"].push(item);
+      });
+    } else {
+      _.extend(additional_options, {
+        extra_menu: {
+          title: "Action",
+          items: export_items,
+        },
+      });
+    }
+
+    return self.open_exclusive_tab_view_aux(
+      filtered_json,
+      custom_name ? custom_name(cluster_id) : "Cluster " + cluster_id,
+      additional_options
+    );
+  };
+
+  self.open_exclusive_tab_view_aux = function (
+    filtered_json,
+    title,
+    option_extras
+  ) {
+    var random_prefix = misc.random_id();
+    var random_tab_id = random_prefix + "_tab";
+    var random_content_id = random_prefix + "_div";
+    var random_button_bar = random_prefix + "_ui";
+
+    while (
+      $("#" + random_tab_id).length ||
+      $("#" + random_content_id).length ||
+      $("#" + random_button_bar).length
+    ) {
+      random_prefix = misc.random_id();
+      random_tab_id = random_prefix + "_tab";
+      random_content_id = random_prefix + "_div";
+      random_button_bar = random_prefix + "_ui";
+    }
+
+    var tab_container = "top_level_tab_container";
+    var content_container = "top_level_tab_content";
+    var go_here_when_closed = "#trace-default-tab";
+
+    // add new tab to the menu bar and switch to it
+    var new_tab_header = $("<li></li>").attr("id", random_tab_id);
+
+    var new_link = $("<a></a>")
+      .attr("href", "#" + random_content_id)
+      .attr("data-toggle", "tab")
+      .text(title);
+    $(
+      '<button type="button" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button>'
+    )
+      .appendTo(new_link)
+      .on("click", () => {
+        self.open_exclusive_tab_close(
+          random_tab_id,
+          random_content_id,
+          go_here_when_closed
+        );
+      });
+
+    new_link.appendTo(new_tab_header);
+    $("#" + tab_container).append(new_tab_header);
+
+    var new_tab_content = $("<div></div>")
+      .addClass("tab-pane")
+      .attr("id", random_content_id)
+      .data("cluster", option_extras.cluster_id);
+
+    if (option_extras.type === "subcluster") {
+      new_tab_content
+        .addClass("subcluster-view")
+        .addClass("subcluster-" + option_extras.cluster_id.replace(".", "_"));
+    }
+
+    //     <li class='disabled' id="attributes-tab"><a href="#trace-attributes" data-toggle="tab">Attributes</a></li>
+    var new_button_bar;
+    if (filtered_json) {
+      new_button_bar = $('[data-hivtrace="cluster-clone"]')
+        .clone()
+        .attr("data-hivtrace", null);
+      new_button_bar
+        .find("[data-hivtrace-button-bar='yes']")
+        .attr("id", random_button_bar)
+        .addClass("cloned-cluster-tab")
+        .attr("data-hivtrace-button-bar", null);
+
+      new_button_bar.appendTo(new_tab_content);
+    }
+    new_tab_content.appendTo("#" + content_container);
+
+    $(new_link).on("show.bs.tab", (e) => {
+      //console.log (e);
+      if (e.relatedTarget) {
+        //console.log (e.relatedTarget);
+        go_here_when_closed = e.relatedTarget;
+      }
+    });
+
+    // show the new tab
+    $(new_link).tab("show");
+
+    var cluster_view;
+
+    if (filtered_json) {
+      var cluster_options = {
+        no_cdc: options && options["no_cdc"],
+        "minimum size": 0,
+        secondary: true,
+        prefix: random_prefix,
+        extra_menu:
+          options && "extra_menu" in options ? options["extra_menu"] : null,
+        "edge-styler":
+          options && "edge-styler" in options ? options["edge-styler"] : null,
+        "no-subclusters": true,
+        "no-subcluster-compute": false,
+      };
+
+      if (option_extras) {
+        _.extend(cluster_options, option_extras);
+      }
+
+      if (
+        option_extras.showing_on_map &&
+        self.countryCentersObject &&
+        self.countryOutlines
+      ) {
+        cluster_options["showing_on_map"] = true;
+        cluster_options["country-centers"] = self.countryCentersObject;
+        cluster_options["country-outlines"] = self.countryOutlines;
+
+        // Create an array of the countries in the selected cluster for use in styling the map.
+        if ("extra-graphics" in cluster_options) {
+          var draw_map = function (other_code, network) {
+            other_code(network);
+            return network._draw_topomap();
+          };
+
+          cluster_options["extra-graphics"] = _.wrap(
+            draw_map,
+            cluster_options["extra-graphics"]
+          );
+        } else {
+          cluster_options["extra-graphics"] = function (network) {
+            return network._draw_topomap();
+          };
+        }
+      }
+
+      cluster_options["today"] = self.today;
+
+      cluster_view = hivtrace_cluster_network_graph(
+        filtered_json,
+        "#" + random_content_id,
+        null,
+        null,
+        random_button_bar,
+        attributes,
+        null,
+        null,
+        null,
+        parent_container,
+        cluster_options
+      );
+
+      if (self.colorizer["category_id"]) {
+        if (self.colorizer["continuous"]) {
+          cluster_view.handle_attribute_continuous(
+            self.colorizer["category_id"]
+          );
+        } else {
+          cluster_view.handle_attribute_categorical(
+            self.colorizer["category_id"]
+          );
+        }
+      }
+
+      if (self.node_shaper["id"]) {
+        cluster_view.handle_shape_categorical(self.node_shaper["id"]);
+      }
+
+      if (self.colorizer["opacity_id"]) {
+        cluster_view.handle_attribute_opacity(self.colorizer["opacity_id"]);
+      }
+
+      cluster_view.expand_cluster_handler(cluster_view.clusters[0], true);
+    } else {
+      return new_tab_content.attr("id");
+    }
+    return cluster_view;
+  };
+
+  // ensure all checkboxes are unchecked at initialization
+  $('input[type="checkbox"]').prop("checked", false);
+
+  var handle_node_click = function (node) {
+    if (d3.event.defaultPrevented) return;
+    var container = d3.select(self.container);
+    var id = "d3_context_menu_id";
+    var menu_object = container.select("#" + id);
+
+    if (menu_object.empty()) {
+      menu_object = container
+        .append("ul")
+        .attr("id", id)
+        .attr("class", "dropdown-menu")
+        .attr("role", "menu");
+    }
+
+    menu_object.selectAll("li").remove();
+
+    if (node) {
+      node.fixed = 1;
+      menu_object
+        .append("li")
+        .append("a")
+        .attr("tabindex", "-1")
+        .text(__("clusters_main")["collapse_cluster"])
+        .on("click", (d) => {
+          node.fixed = 0;
+          collapse_cluster_handler(node, true);
+          menu_object.style("display", "none");
+        });
+
+      menu_object
+        .append("li")
+        .append("a")
+        .attr("tabindex", "-1")
+        .text((d) => (node.show_label ? "Hide text label" : "Show text label"))
+        .on("click", (d) => {
+          node.fixed = 0;
+          //node.show_label = !node.show_label;
+          handle_node_label(container, node);
+          //collapse_cluster_handler(node, true);
+          menu_object.style("display", "none");
+        });
+
+      if (clustersOfInterest.get_editor()) {
+        menu_object
+          .append("li")
+          .append("a")
+          .attr("tabindex", "-1")
+          .text((d) => "Add this node to the cluster of interest")
+          .on("click", (d) => {
+            clustersOfInterest.get_editor().append_node(node.id, true);
+          });
+      }
+
+      // SW20180605 : To be implemented
+
+      //menu_object
+      //  .append("li")
+      //  .append("a")
+      //  .attr("tabindex", "-1")
+      //  .text("Show sequences used to make cluster")
+      //  .on("click", function(d) {
+      //    node.fixed = 0;
+      //    show_sequences_in_cluster (node, true);
+      //    menu_object.style("display", "none");
+      //  });
+
+      menu_object
+        .style("position", "absolute")
+        .style("left", String(d3.event.offsetX) + "px")
+        .style("top", String(d3.event.offsetY) + "px")
+        .style("display", "block");
+    } else {
+      menu_object.style("display", "none");
+    }
+
+    container.on(
+      "click",
+      (d) => {
+        handle_node_click(null);
+      },
+      true
+    );
+  };
+
+  function get_initial_xy(packed) {
+    // create clusters from nodes
+    var mapped_clusters = get_all_clusters(self.nodes);
+
+    var d_clusters = {
+      id: "root",
+      children: [],
+    };
+
+    // filter out clusters that are to be excluded
+    if (self.exclude_cluster_ids) {
+      mapped_clusters = _.omit(mapped_clusters, self.exclude_cluster_ids);
+    }
+
+    d_clusters.children = _.map(mapped_clusters, (value, key) => ({
+      cluster_id: key,
+      children: value,
+    }));
+
+    var treemap = packed
+      ? d3.layout
+          .pack()
+          .size([self.width, self.height])
+          //.sticky(true)
+          .children((d) => d.children)
+          .value((d) => d.parent.children.length ** 1.5)
+          .sort((a, b) => b.value - a.value)
+          .padding(5)
+      : d3.layout
+          .treemap()
+          .size([self.width, self.height])
+          //.sticky(true)
+          .children((d) => d.children)
+          .value((d) => d.parent.children.length ** 1.0)
+          .sort((a, b) => a.value - b.value)
+          .ratio(1);
+
+    var clusters = treemap.nodes(d_clusters);
+    _.each(clusters, (c) => {
+      //c.fixed = true;
+    });
+    return clusters;
+  }
+
+  function prepare_data_to_graph() {
+    var graphMe = {};
+    graphMe.all = [];
+    graphMe.edges = [];
+    graphMe.nodes = [];
+    graphMe.clusters = [];
+
+    var expandedClusters = [];
+    var drawnNodes = [];
+
+    self.clusters.forEach((x) => {
+      if (self.cluster_display_filter(x)) {
+        // Check if hxb2_linked is in a child
+        var hxb2_exists =
+          x.children.some((c) => c.hxb2_linked) && self.hide_hxb2;
+        if (!hxb2_exists) {
+          if (x.collapsed) {
+            graphMe.clusters.push(x);
+            graphMe.all.push(x);
+          } else {
+            expandedClusters[x.cluster_id] = true;
+          }
+        }
+      }
+    });
+
+    self.nodes.forEach((x, i) => {
+      if (expandedClusters[x.cluster]) {
+        drawnNodes[i] = graphMe.nodes.length + graphMe.clusters.length;
+        graphMe.nodes.push(x);
+        graphMe.all.push(x);
+      }
+    });
+
+    self.edges.forEach((x) => {
+      if (!(x.removed && self.filter_edges)) {
+        if (
+          drawnNodes[x.source] !== undefined &&
+          drawnNodes[x.target] !== undefined
+        ) {
+          var y = {};
+          for (var prop in x) {
+            y[prop] = x[prop];
+          }
+
+          y.source = drawnNodes[x.source];
+          y.target = drawnNodes[x.target];
+          y.ref = x;
+          graphMe.edges.push(y);
+        }
+      }
+    });
+
+    return graphMe;
+  }
+
+  self._refresh_subcluster_view = function (set_date) {
+    self.annotate_priority_clusters(
+      timeDateUtil._networkCDCDateField,
+      36,
+      12,
+      set_date
+    );
+
+    var field_def = self.define_attribute_COI_membership(self, set_date);
+
+    //console.log (field_def.dimension);
+
+    if (field_def) {
+      _.each(self.nodes, (node) => {
+        const attr_v = field_def["map"](node, self);
+        HTX.HIVTxNetwork.inject_attribute_node_value_by_id(
+          node,
+          "subcluster_temporal_view",
+          attr_v
+        );
+      });
+
+      self.inject_attribute_description("subcluster_temporal_view", field_def);
+      self._aux_process_category_values(
+        self._aux_populate_category_fields(
+          field_def,
+          "subcluster_temporal_view"
+        )
+      );
+      self.handle_attribute_categorical("subcluster_temporal_view");
+    }
+  };
+
+  self.view_subcluster = function (
+    cluster,
+    custom_filter,
+    custom_name,
+    view_sub_options,
+    custom_edge_filter,
+    include_injected_edges,
+    length_threshold
+  ) {
+    length_threshold = length_threshold || self.subcluster_threshold;
+    let nodes = cluster.children;
+    if (custom_filter) {
+      if (_.isArray(custom_filter)) {
+        nodes = custom_filter;
+      } else {
+        nodes = _.filter(self.json.Nodes, custom_filter);
+      }
+    }
+    var filtered_json = self.extract_single_cluster(
+      nodes,
+      custom_edge_filter || ((e) => e.length <= length_threshold),
+      false,
+      null,
+      include_injected_edges
+    );
+
+    _.each(filtered_json.Nodes, (n) => {
+      n.subcluster_label = "1.1";
+    });
+
+    if (kGlobals.network.GraphAttrbuteID in json) {
+      filtered_json[kGlobals.network.GraphAttrbuteID] = {};
+      $.extend(
+        true,
+        filtered_json[kGlobals.network.GraphAttrbuteID],
+        json[kGlobals.network.GraphAttrbuteID]
+      );
+    }
+
+    view_sub_options = view_sub_options || {};
+
+    view_sub_options["parent_graph"] = self;
+
+    var extra_menu_items = [
+      [
+        function (network, item) {
+          var enclosure = item.append("div").classed("form-group", true);
+          enclosure
+            .append("label")
+            .text("Recalculate National Priority from ")
+            .classed("control-label", true);
+          enclosure
+            .append("input")
+            .attr("type", "date")
+            .classed("form-control", true)
+            .attr("value", timeDateUtil.DateViewFormatSlider(self.today))
+            .attr("max", timeDateUtil.DateViewFormatSlider(self.today))
+            .attr(
+              "min",
+              timeDateUtil.DateViewFormatSlider(
+                d3.min(network.nodes, (node) =>
+                  network.attribute_node_value_by_id(
+                    node,
+                    timeDateUtil._networkCDCDateField
+                  )
+                )
+              )
+            )
+            .on("change", function (e) {
+              //d3.event.preventDefault();
+              var set_date = timeDateUtil.DateViewFormatSlider.parse(
+                this.value
+              );
+              if (this.value) {
+                network._refresh_subcluster_view(set_date);
+
+                enclosure
+                  .classed("has-success", true)
+                  .classed("has-error", false);
+              } else {
+                enclosure
+                  .classed("has-success", false)
+                  .classed("has-error", true);
+              }
+            })
+            .on("click", (e) => {
+              d3.event.stopPropagation();
+            });
+        },
+        null,
+      ],
+    ];
+    if (!self._is_CDC_executive_mode) {
+      extra_menu_items.push([
+        "Export cluster to .CSV",
+        function (network) {
+          helpers.export_csv_button(
+            network._extract_attributes_for_nodes(
+              network._extract_nodes_by_id("1.1"),
+              network._extract_exportable_attributes()
+            )
+          );
+        },
+      ]);
+    }
+
+    view_sub_options["type"] = "subcluster";
+    view_sub_options["cluster_id"] = cluster.cluster_id || "N/A";
+    if ("extra_menu" in view_sub_options) {
+      view_sub_options["extra_menu"]["items"] =
+        view_sub_options["extra_menu"]["items"].concat(extra_menu_items);
+    } else {
+      view_sub_options["extra_menu"] = {
+        title: "Action",
+        items: extra_menu_items,
+      };
+    }
+
+    //self._check_for_time_series(extra_menu_items);
+    var cluster_view = self.open_exclusive_tab_view_aux(
+      filtered_json,
+      custom_name || "Subcluster " + cluster.cluster_id,
+      view_sub_options
+    );
+    if (!view_sub_options.skip_recent_rapid)
+      cluster_view.handle_attribute_categorical("subcluster_or_priority_node");
+    return cluster_view;
+
+    /*var selector =
+      ".subcluster-" +
+      cluster.id.replace(".", "_") +
+      " .show-small-clusters-button";
+
+    var item = $(
+      '<span class="input-group-addon btn view-parent-btn">View Parent</span>'
+    )
+      .data("cluster_id", cluster.parent_cluster.cluster_id)
+      .insertAfter(selector);
+
+    item.on("click", function(e) {
+      self.open_exclusive_tab_view($(this).data("cluster_id"));
+    });*/
+  };
+
+  var oldest_nodes_first = function (n1, n2) {
+    const date_field = date_field || timeDateUtil._networkCDCDateField;
+
+    // consistent node sorting, older nodes first
+    var node1_dx = self.attribute_node_value_by_id(n1, date_field);
+    var node2_dx = self.attribute_node_value_by_id(n2, date_field);
+
+    if (node1_dx === node2_dx) {
+      return n1.id < n2.id ? -1 : 1;
+    }
+    return node1_dx < node2_dx ? -1 : 1;
+  };
+
+  self.annotate_priority_clusters = function (
+    date_field,
+    span_months,
+    recent_months,
+    start_date
+  ) {
+    /* 
+        values for priority_flag
+            0: 0.5% subcluster
+            1: last 12 months NOT in a priority cluster
+            2: last 12 month IN priority cluster
+            3: in priority cluster but not in 12 months
+            4-7 is only computed for start dates different from the network date
+            4: date present but is in the FUTURE compared to start_date
+            5: date present but is between 1900 and start_date
+            6: date missing
+            7: in 0.5% cluster 12<dx<36 months but not a CoI
+            
+            
+        SLKP 20221128:
+            Add a calculation for simple classification of priority clusters
+            
+            0: not in a national priority CoI
+            1: IN a national priority CoI ≤12 months
+            2: IN a national priority CoI 12 - 36 months
+            3: IN a national priority CoI >36 months
+    */
+
+    try {
+      start_date = start_date || self.get_reference_date();
+
+      var cutoff_long = timeDateUtil.n_months_ago(start_date, span_months);
+      var cutoff_short = timeDateUtil.n_months_ago(start_date, recent_months);
+
+      var node_iterator;
+
+      if (start_date === self.today) {
+        node_iterator = self.nodes;
+      } else {
+        var beginning_of_time = timeDateUtil.getCurrentDate();
+        beginning_of_time.setYear(1900);
+        node_iterator = [];
+        _.each(self.nodes, (node) => {
+          var filter_result = self.filter_by_date(
+            beginning_of_time,
+            date_field,
+            start_date,
+            node
+            //true
+          );
+          if (_.isUndefined(filter_result)) {
+            node.priority_flag = 6;
+          } else if (filter_result) {
+            node.priority_flag = 5;
+            node_iterator.push(node);
+          } else {
+            node.priority_flag = 4;
+          }
+        });
+      }
+
+      // extract all clusters at once to avoid inefficiencies of multiple edge-set traversals
+
+      var split_clusters = {};
+      var node_id_to_local_cluster = {};
+
+      // reset all annotations
+
+      _.each(node_iterator, (node) => {
+        node.nationalCOI = 0;
+        if (node.cluster) {
+          if (!(node.cluster in split_clusters)) {
+            split_clusters[node.cluster] = { Nodes: [], Edges: [] };
+          }
+          node_id_to_local_cluster[node.id] =
+            split_clusters[node.cluster]["Nodes"].length;
+          split_clusters[node.cluster]["Nodes"].push(node);
+        }
+      });
+
+      _.each(self.edges, (edge) => {
+        if (edge.length <= self.subcluster_threshold) {
+          var edge_cluster = self.nodes[edge.source].cluster;
+
+          var source_id = self.nodes[edge.source].id;
+          var target_id = self.nodes[edge.target].id;
+
+          if (
+            source_id in node_id_to_local_cluster &&
+            target_id in node_id_to_local_cluster
+          ) {
+            var copied_edge = _.clone(edge);
+
+            copied_edge.source = node_id_to_local_cluster[source_id];
+            copied_edge.target = node_id_to_local_cluster[target_id];
+
+            split_clusters[edge_cluster]["Edges"].push(copied_edge);
+          }
+        }
+      });
+
+      const cluster_id_match =
+        self.precomputed_subclusters &&
+        self.subcluster_threshold in self.precomputed_subclusters
+          ? self.precomputed_subclusters
+          : null;
+
+      _.each(split_clusters, (cluster_nodes, cluster_index) => {
+        /** extract subclusters; all nodes at given threshold */
+        /** Sub-Cluster: all nodes connected at 0.005 subs/site; there can be multiple sub-clusters per cluster */
+
+        //var cluster_nodes       = self.extract_single_cluster (cluster.children, null, true);
+
+        var array_index = self.cluster_mapping[cluster_index];
+
+        self.clusters[array_index].priority_score = 0;
+
+        var edges = [];
+
+        /** all clusters with more than one member connected at 'threshold' edge length */
+        var subclusters = _.filter(
+          hivtrace_cluster_depthwise_traversal(
+            cluster_nodes.Nodes,
+            cluster_nodes.Edges,
+            null,
+            edges
+          ),
+          (cc) => cc.length > 1
+        );
+
+        /** all edge sets with more than one edge */
+        edges = _.filter(edges, (es) => es.length > 1);
+
+        /** sort subclusters by oldest node */
+        _.each(subclusters, (c, i) => {
+          c.sort(oldest_nodes_first);
+        });
+
+        subclusters.sort((c1, c2) => oldest_nodes_first(c1[0], c2[0]));
+
+        let next_id = subclusters.length + 1;
+
+        subclusters = _.map(subclusters, (c, i) => {
+          let subcluster_id = i + 1;
+
+          if (cluster_id_match) {
+            const precomputed_values = {};
+            _.each(c, (n) => {
+              if ("subcluster" in n) {
+                var sub_at_k = _.find(
+                  n.subcluster,
+                  (t) => t[0] === self.subcluster_threshold
+                );
+                if (sub_at_k) {
+                  precomputed_values[
+                    sub_at_k[1].split(kGlobals.SubclusterSeparator)[1]
+                  ] = 1;
+                  return;
+                }
+              }
+
+              precomputed_values[null] = 1;
+            });
+
+            if (
+              null in precomputed_values ||
+              _.keys(precomputed_values).length !== 1
+            ) {
+              subcluster_id = next_id++;
+            } else {
+              subcluster_id = _.keys(precomputed_values)[0];
+            }
+
+            /*if ((i+1) !== 0 + subcluster_id) {
+                console.log (self.clusters[array_index].cluster_id, i, "=>", subcluster_id, _.keys(precomputed_values));
+             }*/
+          }
+
+          var label =
+            self.clusters[array_index].cluster_id +
+            kGlobals.SubclusterSeparator +
+            subcluster_id;
+
+          _.each(c, (n) => {
+            //if (!("subcluster_label" in n)) {
+            n.subcluster_label = label;
+            //}
+            n.priority_flag = 0;
+          });
+
+          return {
+            children: _.clone(c),
+            parent_cluster: self.clusters[array_index],
+            cluster_id: label,
+            distances: helpers.describe_vector(
+              _.map(edges[i], (e) => e.length)
+            ),
+          };
+        });
+
+        _.each(subclusters, (c) => {
+          _compute_cluster_degrees(c);
+        });
+
+        self.clusters[array_index].subclusters = subclusters;
+
+        /** now, for each subcluster, extract the recent and rapid part */
+
+        /** Recent & Rapid (National Priority) Cluster: the part of the Sub-Cluster inferred using only cases diagnosed in the previous 36 months
+                and at least two cases dx-ed in the previous 12 months; there is a path between all nodes in a National Priority Cluster
+
+            20180406 SLKP: while unlikely, this definition could result in multiple National Priority clusters
+            per subclusters; for now we will add up all the cases for prioritization, and
+            display the largest National Priority cluster if there is more than one
+        */
+
+        _.each(subclusters, (sub) => {
+          // extract nodes based on dates
+
+          const date_filter = (n) =>
+            self.filter_by_date(cutoff_long, date_field, start_date, n);
+
+          var subcluster_json = self.extract_single_cluster(
+            _.filter(sub.children, date_filter),
+            null,
+            true,
+            cluster_nodes
+          );
+
+          var rr_cluster = _.filter(
+            hivtrace_cluster_depthwise_traversal(
+              subcluster_json.Nodes,
+              _.filter(
+                subcluster_json.Edges,
+                (e) => e.length <= self.subcluster_threshold
+              )
+            ),
+            (cc) => cc.length > 1
+          );
+
+          sub.rr_count = rr_cluster.length;
+
+          rr_cluster.sort((a, b) => b.length - a.length);
+
+          sub.priority_score = [];
+          sub.recent_nodes = [];
+
+          const future_date = new Date(start_date.getTime() + 1e13);
+
+          _.each(rr_cluster, (recent_cluster) => {
+            var priority_nodes = _.groupBy(recent_cluster, (n) =>
+              self.filter_by_date(cutoff_short, date_field, start_date, n)
+            );
+
+            sub.recent_nodes.push(_.map(recent_cluster, (n) => n.id));
+            const meets_priority_def =
+              true in priority_nodes &&
+              priority_nodes[true].length >=
+                (self.CDC_data
+                  ? self.CDC_data["autocreate-priority-set-size"]
+                  : 3);
+
+            if (true in priority_nodes) {
+              // recent
+              sub.priority_score.push(_.map(priority_nodes[true], (n) => n.id));
+              _.each(priority_nodes[true], (n) => {
+                n.priority_flag = self.filter_by_date(
+                  start_date,
+                  date_field,
+                  future_date,
+                  n
+                )
+                  ? 4
+                  : 1;
+
+                if (meets_priority_def) {
+                  if (n.priority_flag === 1) {
+                    n.priority_flag = 2;
+                  }
+                  n.nationalCOI = 1;
+                }
+              });
+            }
+
+            if (false in priority_nodes) {
+              // not recent
+              _.each(priority_nodes[false], (n) => {
+                n.priority_flag = 3;
+
+                if (meets_priority_def) {
+                  if (
+                    self.filter_by_date(cutoff_long, date_field, start_date, n)
+                  ) {
+                    n.nationalCOI = 2;
+                  } else {
+                    n.nationalCOI = 3;
+                  }
+                } else {
+                  n.priority_flag = 7;
+                }
+              });
+            }
+          });
+
+          //console.log (sub.recent_nodes);
+          self.clusters[array_index].priority_score = sub.priority_score;
+        });
+      });
+    } catch (err) {
+      console.log(err);
+    }
+  };
+
+  function default_layout(packed) {
+    // let's create an array of clusters from the json
+
+    var init_layout = get_initial_xy(packed);
+
+    if (self.clusters.length === 0) {
+      self.clusters = init_layout.filter(
+        (v, i, obj) => !(typeof v.cluster_id === "undefined")
+      );
+    } else {
+      var coordinate_update = {};
+      _.each(self.clusters, (c) => {
+        coordinate_update[c.cluster_id] = c;
+      });
+      _.each(init_layout, (c) => {
+        if ("cluster_id" in c) {
+          _.extendOwn(coordinate_update[c.cluster_id], c);
+        }
+      });
+    }
+
+    //var sizes = network_layout.size();
+
+    var set_init_coords = packed
+      ? function (n) {
+          n.x += n.r * 0.5;
+          n.y += n.r * 0.5;
+        }
+      : function (n) {
+          n.x += n.dx * 0.5;
+          n.y += n.dy * 0.5;
+        };
+
+    _.each([self.nodes, self.clusters], (list) => {
+      _.each(list, set_init_coords);
+    });
+
+    self.clusters.forEach(collapse_cluster);
+  }
+
+  function change_spacing(delta) {
+    self.charge_correction *= delta;
+    network_layout.start();
+  }
+
+  function change_window_size(delta, trigger) {
+    if (delta) {
+      var x_scale = (self.width + delta / 2) / self.width;
+      var y_scale = (self.height + delta / 2) / self.height;
+
+      self.width += delta;
+      self.height += delta;
+
+      var rescale_x = d3.scale
+        .linear()
+        .domain(d3.extent(network_layout.nodes(), (node) => node.x));
+      rescale_x.range(_.map(rescale_x.domain(), (v) => v * x_scale));
+      //.range ([50,self.width-50]),
+      var rescale_y = d3.scale
+        .linear()
+        .domain(d3.extent(network_layout.nodes(), (node) => node.y));
+      rescale_y.range(_.map(rescale_y.domain(), (v) => v * y_scale));
+
+      _.each(network_layout.nodes(), (node) => {
+        node.x = rescale_x(node.x);
+        node.y = rescale_y(node.y);
+      });
+    }
+
+    self.width = Math.min(Math.max(self.width, 200), 4000);
+    self.height = Math.min(Math.max(self.height, 200), 4000);
+
+    network_layout.size([self.width, self.height]);
+    self.network_svg.attr("width", self.width).attr("height", self.height);
+    self._calc_country_nodes(options);
+    self._draw_topomap(true);
+    if (trigger) {
+      network_layout.start();
+    } else if (delta) {
+      self.update(true);
+    }
+  }
+
+  self.compute_adjacency_list = _.once(() => {
+    self.nodes.forEach((n) => {
+      n.neighbors = d3.set();
+    });
+
+    self.edges.forEach((e) => {
+      self.nodes[e.source].neighbors.add(e.target);
+      self.nodes[e.target].neighbors.add(e.source);
+    });
+  });
+
+  self.compute_local_clustering_coefficients = _.once(() => {
+    self.compute_adjacency_list();
+
+    self.nodes.forEach((n) => {
+      _.defer((a_node) => {
+        const neighborhood_size = a_node.neighbors.size();
+        if (neighborhood_size < 2) {
+          a_node.lcc = misc.undefined;
+        } else if (neighborhood_size > 500) {
+          a_node.lcc = misc.too_large;
+        } else {
+          // count triangles
+          const neighborhood = a_node.neighbors.values();
+          let counter = 0;
+          for (let n1 = 0; n1 < neighborhood_size; n1 += 1) {
+            for (let n2 = n1 + 1; n2 < neighborhood_size; n2 += 1) {
+              if (
+                self.nodes[neighborhood[n1]].neighbors.has(neighborhood[n2])
+              ) {
+                counter++;
+              }
+            }
+          }
+
+          a_node.lcc =
+            (2 * counter) / neighborhood_size / (neighborhood_size - 1);
+        }
+      }, n);
+    });
+  });
+
+  self.get_node_by_id = function (id) {
+    return self.nodes.filter((n) => n.id === id)[0];
+  };
+
+  self.compute_local_clustering_coefficients_worker = _.once(() => {
+    var worker = new Worker("workers/lcc.js");
+
+    worker.onmessage = function (event) {
+      var nodes = event.data.Nodes;
+
+      nodes.forEach((n) => {
+        const node_to_update = self.get_node_by_id(n.id);
+        node_to_update.lcc = n.lcc ? n.lcc : misc.undefined;
+      });
+    };
+
+    var worker_obj = {};
+    worker_obj["Nodes"] = self.nodes;
+    worker_obj["Edges"] = self.edges;
+    worker.postMessage(worker_obj);
+  });
+
+  var estimate_cubic_compute_cost = _.memoize(
+    (c) => {
+      self.compute_adjacency_list();
+      return _.reduce(
+        _.first(_.pluck(c.children, "degree").sort(d3.descending), 3),
+        (memo, value) => memo * value,
+        1
+      );
+    },
+    (c) => c.cluster_id
+  );
+
+  self.compute_global_clustering_coefficients = _.once(() => {
+    self.compute_adjacency_list();
+
+    self.clusters.forEach((c) => {
+      _.defer((a_cluster) => {
+        const cluster_size = a_cluster.children.length;
+        if (cluster_size < 3) {
+          a_cluster.cc = misc.undefined;
+        } else if (estimate_cubic_compute_cost(a_cluster, true) >= 5000000) {
+          a_cluster.cc = misc.too_large;
+        } else {
+          // pull out all the nodes that have this cluster id
+          const member_nodes = [];
+
+          var triads = 0;
+          var triangles = 0;
+
+          self.nodes.forEach((n, i) => {
+            if (n.cluster === a_cluster.cluster_id) {
+              member_nodes.push(i);
+            }
+          });
+          member_nodes.forEach((node) => {
+            const my_neighbors = self.nodes[node].neighbors
+              .values()
+              .map((d) => Number(d))
+              .sort(d3.ascending);
+            for (let n1 = 0; n1 < my_neighbors.length; n1 += 1) {
+              for (let n2 = n1 + 1; n2 < my_neighbors.length; n2 += 1) {
+                triads += 1;
+                if (
+                  self.nodes[my_neighbors[n1]].neighbors.has(my_neighbors[n2])
+                ) {
+                  triangles += 1;
+                }
+              }
+            }
+          });
+
+          a_cluster.cc = triangles / triads;
+        }
+      }, c);
+    });
+  });
+
+  self.mark_nodes_as_processing = function (property) {
+    self.nodes.forEach((n) => {
+      n[property] = misc.processing;
+    });
+  };
+
+  self.compute_graph_stats = function () {
+    d3.select(this).classed("disabled", true).select("i").classed({
+      "fa-calculator": false,
+      "fa-cog": true,
+      "fa-spin": true,
+    });
+    self.mark_nodes_as_processing("lcc");
+    self.compute_local_clustering_coefficients_worker();
+    self.compute_global_clustering_coefficients();
+    d3.select(this).remove();
+  };
+
+  /*------------ Constructor ---------------*/
+  function initial_json_load() {
+    var connected_links = {};
+    var total = 0;
+    self.exclude_cluster_ids = {};
+    self.has_hxb2_links = false;
+    self.cluster_sizes = [];
+
+    graph_data.Nodes.forEach((d) => {
+      if (typeof self.cluster_sizes[d.cluster - 1] === "undefined") {
+        self.cluster_sizes[d.cluster - 1] = 1;
+      } else {
+        self.cluster_sizes[d.cluster - 1]++;
+      }
+      if ("is_lanl" in d) {
+        d.is_lanl = d.is_lanl === "true";
+      }
+
+      if (!("attributes" in d)) {
+        d.attributes = [];
+      }
+
+      if (d.attributes.indexOf("problematic") >= 0) {
+        self.has_hxb2_links = true;
+        d.hxb2_linked = true;
+      }
+    });
+
+    /* add buttons and handlers */
+    /* clusters first */
+
+    self._extract_attributes_for_nodes = function (nodes, column_names) {
+      var result = [_.map(column_names, (c) => c.raw_attribute_key)];
+
+      _.each(nodes, (n) => {
+        result.push(
+          _.map(column_names, (c) => {
+            if (c.raw_attribute_key === tables._networkNodeIDField) {
+              if (HTX.HIVTxNetwork.is_new_node(n)) {
+                return n.id + tables._networkNewNodeMarker;
+              }
+              return n.id;
+            }
+            if (_.has(n, c.raw_attribute_key)) {
+              return n[c.raw_attribute_key];
+            }
+            return self.attribute_node_value_by_id(n, c.raw_attribute_key);
+          })
+        );
+      });
+      return result;
+    };
+
+    self._extract_exportable_attributes = function (extended) {
+      var allowed_types = {
+        String: 1,
+        Date: 1,
+        Number: 1,
+      };
+
+      var return_array = [];
+
+      if (extended) {
+        return_array = [
+          {
+            raw_attribute_key: tables._networkNodeIDField,
+            type: "String",
+            label: "Node ID",
+            format: function () {
+              return "Node ID";
+            },
+          },
+          {
+            raw_attribute_key: "cluster",
+            type: "String",
+            label: "Which cluster the individual belongs to",
+            format: function () {
+              return __("clusters_tab")["cluster_id"];
+            },
+          },
+        ];
+      }
+
+      return_array.push(
+        _.filter(
+          self.json[kGlobals.network.GraphAttrbuteID],
+          (d) => d.type in allowed_types
+        )
+      );
+
+      return _.flatten(return_array, true);
+    };
+
+    self._extract_nodes_by_id = function (id) {
+      return _.filter(
+        self.nodes,
+        (n) =>
+          n.cluster.toString() === id.toString() ||
+          n.subcluster_label === id.toString()
+      );
+    };
+
+    self._cluster_list_view_render = function (
+      cluster_id,
+      group_by_attribute,
+      the_list,
+      priority_group
+    ) {
+      the_list.selectAll("*").remove();
+      var column_ids = self._extract_exportable_attributes();
+      var cluster_nodes;
+
+      if (priority_group) {
+        cluster_nodes = self.priority_groups_find_by_name(priority_group);
+        if (cluster_nodes) {
+          cluster_nodes = cluster_nodes.node_objects;
+        } else {
+          return;
+        }
+      } else {
+        cluster_nodes = self._extract_nodes_by_id(cluster_id);
+      }
+
+      d3.select(
+        self.get_ui_element_selector_by_role("cluster_list_data_export", true)
+      ).on("click", (d) => {
+        if (self._is_CDC_executive_mode) {
+          alert(kGlobals.network.WarnExecutiveMode);
+        } else {
+          helpers.export_csv_button(
+            self._extract_attributes_for_nodes(cluster_nodes, column_ids)
+          );
+        }
+      });
+
+      if (group_by_attribute) {
+        _.each(column_ids, (column) => {
+          var binned = _.groupBy(cluster_nodes, (n) =>
+            self.attribute_node_value_by_id(n, column.raw_attribute_key)
+          );
+          var sorted_keys = _.keys(binned).sort();
+          var attribute_record = the_list.append("li");
+          attribute_record
+            .append("code")
+            .text(column.label || column.raw_attribute_key);
+          var attribute_list = attribute_record
+            .append("dl")
+            .classed("dl-horizontal", true);
+          _.each(sorted_keys, (key) => {
+            attribute_list.append("dt").text(key);
+            attribute_list
+              .append("dd")
+              .text(_.map(binned[key], (n) => n.id).join(", "));
+          });
+        });
+      } else {
+        _.each(cluster_nodes, (node) => {
+          var patient_record = the_list.append("li");
+          patient_record.append("code").text(node.id);
+          var patient_list = patient_record
+            .append("dl")
+            .classed("dl-horizontal", true);
+          _.each(column_ids, (column) => {
+            patient_list
+              .append("dt")
+              .text(column.label || column.raw_attribute_key);
+            patient_list
+              .append("dd")
+              .text(
+                self.attribute_node_value_by_id(node, column.raw_attribute_key)
+              );
+          });
+        });
+      }
+    };
+
+    self._setup_cluster_list_view = function () {
+      d3.select(
+        self.get_ui_element_selector_by_role("cluster_list_view_toggle", true)
+      ).on("click", function () {
+        d3.event.preventDefault();
+        var group_by_id;
+
+        var button_clicked = $(this);
+        if (button_clicked.data(__("clusters_tab")["view"]) === "id") {
+          button_clicked.data(__("clusters_tab")["view"], "attribute");
+          button_clicked.text(__("clusters_tab")["group_by_id"]);
+          group_by_id = false;
+        } else {
+          button_clicked.data(__("clusters_tab")["view"], "id");
+          button_clicked.text(__("clusters_tab")["group_by_attribute"]);
+          group_by_id = true;
+        }
+
+        var cluster_id = button_clicked.data("cluster");
+
+        self._cluster_list_view_render(
+          cluster_id ? cluster_id.toString() : "",
+          !group_by_id,
+          d3.select(
+            self.get_ui_element_selector_by_role("cluster_list_payload", true)
+          ),
+          button_clicked.data("priority_list")
+        );
+      });
+
+      $(self.get_ui_element_selector_by_role("cluster_list", true)).on(
+        "show.bs.modal",
+        (event) => {
+          var link_clicked = $(event.relatedTarget);
+          var cluster_id = link_clicked.data("cluster");
+          var priority_list = link_clicked.data("priority_set");
+
+          var modal = d3.select(
+            self.get_ui_element_selector_by_role("cluster_list", true)
+          );
+          modal
+            .selectAll(".modal-title")
+            .text(
+              __("clusters_tab")["listing_nodes"] +
+                (priority_list
+                  ? " in cluster of interest " + priority_list
+                  : " " + __("general")["cluster"] + " " + cluster_id)
+            );
+
+          var view_toggle = $(
+            self.get_ui_element_selector_by_role(
+              "cluster_list_view_toggle",
+              true
+            )
+          );
+
+          if (priority_list) {
+            view_toggle.data("priority_list", priority_list);
+            view_toggle.data("cluster", "");
+          } else {
+            view_toggle.data("cluster", cluster_id);
+            view_toggle.data("priority_list", null);
+          }
+
+          self._cluster_list_view_render(
+            cluster_id,
+            //cluster_id,
+            $(
+              self.get_ui_element_selector_by_role(
+                "cluster_list_view_toggle",
+                true
+              )
+            ).data(__("clusters_tab")["view"]) !== "id",
+            modal.select(
+              self.get_ui_element_selector_by_role("cluster_list_payload", true)
+            ),
+            priority_list
+          );
+        }
+      );
+
+      $(self.get_ui_element_selector_by_role("overlap_list", true)).on(
+        "show.bs.modal",
+        (event) => {
+          var link_clicked = $(event.relatedTarget);
+          var priority_list = link_clicked.data("priority_set");
+
+          var modal = d3.select(
+            self.get_ui_element_selector_by_role("overlap_list", true)
+          );
+          modal
+            .selectAll(".modal-title")
+            .text(
+              "View how nodes in cluster of interest " +
+                priority_list +
+                " overlap with other clusterOI"
+            );
+
+          const ps = self.priority_groups_find_by_name(priority_list);
+          if (!(ps && self.priority_node_overlap)) return;
+
+          var headers = [
+            [
+              {
+                value: "Node",
+                help: "EHARS_ID of the node that overlaps with other clusterOI",
+                sort: "value",
+              },
+              {
+                value: "Other Cluster(s) of Interest",
+                help: "Names of other clusterOI where this node is included",
+                sort: "value",
+              },
+            ],
+          ];
+
+          var rows = [];
+          var rows_for_export = [
+            ["Overlapping Cluster of Interest", "Node", "Other clusterOI"],
+          ];
+          _.each(ps.nodes, (n) => {
+            const overlap = self.priority_node_overlap[n.name];
+            let other_sets = "None";
+            if (overlap.size > 1) {
+              other_sets = _.sortBy(
+                _.filter([...overlap], (d) => d !== priority_list)
+              ).join("; ");
+            }
+            rows.push([{ value: n.name }, { value: other_sets }]);
+            rows_for_export.push([ps.name, n.name, other_sets]);
+          });
+
+          d3.select(
+            self.get_ui_element_selector_by_role(
+              "overlap_list_data_export",
+              true
+            )
+          ).on("click", (d) => {
+            helpers.export_csv_button(rows_for_export, "overlap");
+          });
+
+          tables.add_a_sortable_table(
+            modal.select(
+              self.get_ui_element_selector_by_role(
+                "overlap_list_data_table",
+                true
+              )
+            ),
+            headers,
+            rows,
+            true,
+            null,
+            clustersOfInterest.get_editor()
+          );
+        }
+      );
+    };
+
+    $(self.get_ui_element_selector_by_role("priority_set_merge", true)).on(
+      "show.bs.modal",
+      (event) => {
+        var modal = d3.select(
+          self.get_ui_element_selector_by_role("priority_set_merge", true)
+        );
+
+        const desc = modal.selectAll(".modal-desc");
+
+        const proceed_btn = d3.select(
+          self.get_ui_element_selector_by_role(
+            "priority_set_merge_table_proceed",
+            true
+          )
+        );
+
+        if (
+          self.defined_priority_groups &&
+          self.defined_priority_groups.length > 1
+        ) {
+          desc.text("Select two or more clusters of interest to merge");
+
+          var headers = [
+            [
+              {
+                value: "Select",
+              },
+              {
+                value: "Cluster of interest",
+                help: "Cluster of interest Name",
+                sort: "value",
+              },
+              {
+                value: "Nodes",
+                help: "How many nodes are in this cluster of interest",
+                sort: "value",
+              },
+              {
+                value: "Overlaps",
+                help: "Overlaps with",
+                sort: "value",
+              },
+            ],
+          ];
+
+          const current_selection = new Set();
+          let current_node_set = null;
+          let current_node_objects = null;
+
+          const handle_selection = (name, selected) => {
+            if (selected) {
+              current_selection.add(name);
+            } else {
+              current_selection.delete(name);
+            }
+            if (current_selection.size > 1) {
+              let clusterOITotalNOdes = 0;
+              current_node_set = new Set();
+              current_node_objects = {};
+              _.each(self.defined_priority_groups, (pg) => {
+                if (current_selection.has(pg.name)) {
+                  clusterOITotalNOdes += pg.nodes.length;
+                  _.each(pg.nodes, (n) => {
+                    current_node_set.add(n.name);
+                    current_node_objects[n.name] = {
+                      _priority_set_date: n.added,
+                      _priority_set_kind: n.kind,
+                    };
+                  });
+                }
+              });
+              desc.html(
+                "Merge " +
+                  current_selection.size +
+                  " clusterOI with " +
+                  clusterOITotalNOdes +
+                  " nodes, creating a new clusterOI with " +
+                  current_node_set.size +
+                  " nodes. <br><small>Note that the clusters of interest being merged will <b>not</b> be automatically deleted</small>"
+              );
+              proceed_btn.attr("disabled", null);
+            } else {
+              desc.text("Select two or more clusters of interest to merge");
+              proceed_btn.attr("disabled", "disabled");
+            }
+          };
+
+          const handle_merge = () => {
+            if (current_node_set) {
+              clustersOfInterest.open_editor(
+                self,
+                [],
+                "",
+                "Merged from " + [...current_selection].join(" and ")
+              );
+              clustersOfInterest
+                .get_editor()
+                .append_nodes([...current_node_set], current_node_objects);
+            }
+            $(modal.node()).modal("hide");
+          };
+
+          proceed_btn.attr("disabled", "disabled").on("click", handle_merge);
+
+          var rows = [];
+          _.each(self.defined_priority_groups, (pg) => {
+            const my_overlaps = new Set();
+            _.each(pg.nodes, (n) => {
+              _.each([...self.priority_node_overlap[n.name]], (ps) => {
+                if (ps !== pg.name) {
+                  my_overlaps.add(ps);
+                }
+              });
+            });
+
+            rows.push([
+              {
+                value: pg,
+                callback: function (element, payload) {
+                  var this_cell = d3.select(element);
+                  this_cell
+                    .append("input")
+                    .attr("type", "checkbox")
+                    .style("margin-left", "1em")
+                    .on("click", function (e) {
+                      handle_selection(payload.name, $(this).prop("checked"));
+                    });
+                },
+              },
+              { value: pg.name },
+              { value: pg.nodes.length },
+              {
+                value: [...my_overlaps],
+                format: (d) => d.join("<br>"),
+                html: true,
+              },
+            ]);
+          });
+
+          tables.add_a_sortable_table(
+            modal.select(
+              self.get_ui_element_selector_by_role(
+                "priority_set_merge_table",
+                true
+              )
+            ),
+            headers,
+            rows,
+            true,
+            null,
+            clustersOfInterest.get_editor()
+          );
+        }
+      }
+    );
+
+    if (button_bar_ui) {
+      self._setup_cluster_list_view();
+
+      var cluster_ui_container = d3.select(
+        self.get_ui_element_selector_by_role("cluster_operations_container")
+      );
+
+      cluster_ui_container.selectAll("li").remove();
+
+      var fix_handler = function (do_fix) {
+        _.each([self.clusters, self.nodes], (list) => {
+          _.each(list, (obj) => {
+            obj.fixed = do_fix;
+          });
+        });
+      };
+
+      var node_label_handler = function (do_show) {
+        var shown_nodes = self.network_svg.selectAll(".node");
+        if (!shown_nodes.empty()) {
+          shown_nodes.each((node) => {
+            node.show_label = do_show;
+          });
+          self.update(true);
+        }
+      };
+
+      var layout_reset_handler = function (packed) {
+        var fixed = [];
+        _.each(self.clusters, (obj) => {
+          if (obj.fixed) {
+            fixed.push(obj);
+          }
+          obj.fixed = false;
+        });
+        default_layout(packed);
+        network_layout.tick();
+        self.update();
+        _.each(fixed, (obj) => {
+          obj.fixed = true;
+        });
+      };
+
+      var cluster_commands = [
+        [
+          __("clusters_main")["export_colors"],
+          () => {
+            const colorScheme = helpers.exportColorScheme(
+              self.uniqValues,
+              self.colorizer
+            );
+
+            //TODO: If using database backend, use api instead
+            helpers.copyToClipboard(JSON.stringify(colorScheme));
+          },
+          true,
+          "hivtrace-export-color-scheme",
+        ],
+        [
+          __("clusters_main")["expand_all"],
+          function () {
+            return self.expand_some_clusters();
+          },
+          true,
+          "hivtrace-expand-all",
+        ],
+        [
+          __("clusters_main")["collapse_all"],
+          function () {
+            return self.collapse_some_clusters();
+          },
+          true,
+          "hivtrace-collapse-all",
+        ],
+        [
+          __("clusters_main")["expand_filtered"],
+          function () {
+            return self.expand_some_clusters(
+              self.select_some_clusters((n) => n.match_filter)
+            );
+          },
+          true,
+          "hivtrace-expand-filtered",
+        ],
+        [
+          __("clusters_main")["collapse_filtered"],
+          function () {
+            return self.collapse_some_clusters(
+              self.select_some_clusters((n) => n.match_filter)
+            );
+          },
+          true,
+          "hivtrace-collapse-filtered",
+        ],
+        [
+          __("clusters_main")["fix_all_objects_in_place"],
+          _.partial(fix_handler, true),
+          true,
+          "hivtrace-fix-in-place",
+        ],
+        [
+          __("clusters_main")["allow_all_objects_to_float"],
+          _.partial(fix_handler, false),
+          true,
+          "hivtrace-allow-to-float",
+        ],
+        [
+          __("clusters_main")["reset_layout"] + " [packed]",
+          _.partial(layout_reset_handler, true),
+          true,
+          "hivtrace-reset-layout",
+        ],
+        [
+          __("clusters_main")["reset_layout"] + " [tiled]",
+          _.partial(layout_reset_handler, false),
+          true,
+          "hivtrace-reset-layout",
+        ],
+        [
+          __("network_tab")["show_labels_for_all"],
+          _.partial(node_label_handler, true),
+          true,
+          "hivtrace-node-labels-on",
+        ],
+        [
+          __("network_tab")["hide_labels_for_all"],
+          _.partial(node_label_handler, false),
+          true,
+          "hivtrace-node-labels-off",
+        ],
+        [
+          "Hide problematic clusters",
+          function (item) {
+            d3.select(item).text(
+              self.hide_hxb2
+                ? "Hide problematic clusters"
+                : "Show problematic clusters"
+            );
+            self.toggle_hxb2();
+          },
+          self.has_hxb2_links,
+          "hivtrace-hide-problematic-clusters",
+        ],
+        [
+          __("network_tab")["highlight_unsupported_edges"],
+          function (item) {
+            if (self.highlight_unsuppored_edges) {
+              d3.select(item).selectAll(".fa-check-square").remove();
+            } else {
+              d3.select(item)
+                .insert("i", ":first-child")
+                .classed("fa fa-check-square", true);
+            }
+            self.toggle_highlight_unsupported_edges();
+          },
+          true,
+          "hivtrace-highlight-unsuppored_edges",
+          self.highlight_unsuppored_edges,
+        ],
+      ];
+
+      if (self.cluster_attributes) {
+        cluster_commands.push([
+          "Show only changes since last network update",
+          function (item) {
+            if (self.showing_diff) {
+              d3.select(item).selectAll(".fa-check-square").remove();
+            } else {
+              d3.select(item)
+                .insert("i", ":first-child")
+                .classed("fa fa-check-square", true);
+            }
+            self.toggle_diff();
+          },
+          true,
+          "hivtrace-show-network-diff",
+          self.showing_diff,
+        ]);
+      }
+
+      if (timeDateUtil.getClusterTimeScale()) {
+        cluster_commands.push([
+          __("network_tab")["only_recent_clusters"],
+          function (item) {
+            if (self.using_time_filter) {
+              d3.select(item).selectAll(".fa-check-square").remove();
+            } else {
+              d3.select(item)
+                .insert("i", ":first-child")
+                .classed("fa fa-check-square", true);
+            }
+            self.toggle_time_filter();
+          },
+          true,
+          "hivtrace-show-using-time-filter",
+          self.using_time_filter,
+        ]);
+      }
+
+      if (!self._is_CDC_) {
+        cluster_commands.push([
+          "Show removed edges",
+          function (item) {
+            self.filter_edges = !self.filter_edges;
+            d3.select(item).text(
+              self.filter_edges ? "Show removed edges" : "Hide removed edges"
+            );
+            self.update(false);
+          },
+          function () {
+            return _.some(self.edges, (d) => d.removed);
+          },
+          "hivtrace-show-removed-edges",
+        ]);
+      } else {
+        cluster_commands.push([
+          "Add filtered objects to cluster of interest",
+          function (item) {
+            if (clustersOfInterest.get_editor())
+              clustersOfInterest
+                .get_editor()
+                .append_node_objects(
+                  _.filter(json["Nodes"], (n) => n.match_filter)
+                );
+          },
+          clustersOfInterest.get_editor,
+          "hivtrace-add-filtered-to-panel",
+        ]);
+      }
+
+      cluster_commands.forEach(function (item, index) {
+        let shown = item[2];
+        if (_.isFunction(shown)) {
+          shown = shown(item);
+        }
+        if (shown) {
+          var handler_callback = item[1];
+          var line_item = this.append("li")
+            .append("a")
+            .text(item[0])
+            .attr("href", "#")
+            //.attr("id", item[3])
+            .on("click", function (e) {
+              handler_callback(this);
+              //d3.event.stopPropagation();
+              //d3.event.preventDefault();
+            });
+
+          if (item.length > 4) {
+            // checkbox
+            line_item.text("");
+            if (item[4]) {
+              line_item
+                .insert("i", ":first-child")
+                .classed("fa fa-check-square", true);
+            }
+            line_item.insert("span").text(item[0]);
+          }
+        }
+      }, cluster_ui_container);
+
+      var button_group = d3.select(
+        self.get_ui_element_selector_by_role("button_group")
+      );
+
+      if (!button_group.empty()) {
+        button_group.selectAll("button").remove();
+        button_group
+          .append("button")
+          .classed("btn btn-default btn-sm", true)
+          .attr("title", __("network_tab")["expand_spacing"])
+          .on("click", (d) => {
+            change_spacing(5 / 4);
+          })
+          .append("i")
+          .classed("fa fa-plus", true);
+        button_group
+          .append("button")
+          .classed("btn btn-default btn-sm", true)
+          .attr("title", __("network_tab")["compress_spacing"])
+          .on("click", (d) => {
+            change_spacing(4 / 5);
+          })
+          .append("i")
+          .classed("fa fa-minus", true);
+        button_group
+          .append("button")
+          .classed("btn btn-default btn-sm", true)
+          .attr("title", __("network_tab")["enlarge_window"])
+          .on("click", (d) => {
+            change_window_size(100, true);
+          })
+          .append("i")
+          .classed("fa fa-expand", true);
+        button_group
+          .append("button")
+          .classed("btn btn-default btn-sm", true)
+          .attr("title", __("network_tab")["shrink_window"])
+          .on("click", (d) => {
+            change_window_size(-100, true);
+          })
+          .append("i")
+          .classed("fa fa-compress", true);
+
+        if (!self._is_CDC_) {
+          button_group
+            .append("button")
+            .classed("btn btn-default btn-sm", true)
+            .attr("title", "Compute graph statistics")
+            .attr("id", "hivtrace-compute-graph-statistics")
+            .on("click", function (d) {
+              _.bind(self.compute_graph_stats, this)();
+            })
+            .append("i")
+            .classed("fa fa-calculator", true);
+        } else {
+          button_group
+            .append("button")
+            .classed("btn btn-default btn-sm", true)
+            .attr("title", __("network_tab")["toggle_epicurve"])
+            .attr("id", "hivtrace-toggle-epi-curve")
+            .on("click", (d) => {
+              self.check_for_time_series();
+            })
+            .append("i")
+            .classed("fa fa-line-chart", true);
+        }
+
+        var export_image = d3.select(
+          self.get_ui_element_selector_by_role("export_image")
+        );
+
+        if (!export_image.empty()) {
+          export_image.selectAll("div").remove();
+
+          const buttonGroupDropdown = export_image
+            .insert("div", ":first-child")
+            .classed("input-group-btn dropdown-img", true);
+
+          const dropdownList = buttonGroupDropdown
+            .append("ul")
+            .classed("dropdown-menu", true)
+            .attr("aria-labelledby", "dropdownImg");
+
+          dropdownList
+            .append("li")
+            .classed("dropdown-item export-img-item", true)
+            .append("a")
+            .attr("href", "#")
+            .text("SVG")
+            .on("click", (d) => {
+              helpers.save_image("svg", "#" + self.dom_prefix + "-network-svg");
+            });
+
+          dropdownList
+            .append("li")
+            .classed("dropdown-item export-img-item", true)
+            .append("a")
+            .attr("href", "#")
+            .text("PNG")
+            .on("click", (d) => {
+              helpers.save_image("png", "#" + self.dom_prefix + "-network-svg");
+            });
+
+          const imgBtn = buttonGroupDropdown
+            .append("button")
+            .attr("id", "dropdownImg")
+            .attr("data-toggle", "dropdown")
+            .classed("btn btn-default btn-sm dropdown-toggle", true)
+            .attr("title", __("network_tab")["save_image"])
+            .attr("id", "hivtrace-export-image");
+
+          imgBtn.append("i").classed("fa fa-image", true);
+
+          imgBtn.append("span").classed("caret", true);
+        }
+      }
+
+      $(self.get_ui_element_selector_by_role("filter"))
+        .off("input propertychange")
+        .on(
+          "input propertychange",
+          _.throttle(function (e) {
+            var filter_value = $(this).val();
+            self.filter(tables.filter_parse(filter_value));
+          }, 250)
+        );
+
+      $(self.get_ui_element_selector_by_role("hide_filter"))
+        .off("change")
+        .on(
+          "change",
+          _.throttle((e) => {
+            self.hide_unselected = !self.hide_unselected;
+            self.filter_visibility();
+            self.update(true);
+          }, 250)
+        );
+
+      $(self.get_ui_element_selector_by_role("show_small_clusters"))
+        .off("change")
+        .on(
+          "change",
+          _.throttle((e) => {
+            if ("size" in self.cluster_filtering_functions) {
+              delete self.cluster_filtering_functions["size"];
+            } else {
+              self.cluster_filtering_functions["size"] = self.filter_by_size;
+            }
+
+            self.update(false);
+          }, 250)
+        );
+
+      $(self.get_ui_element_selector_by_role("set_min_cluster_size"))
+        .off("change")
+        .on(
+          "change",
+          _.throttle((e) => {
+            self.minimum_cluster_size = e.target.value;
+            self.update(false);
+          }, 250)
+        );
+
+      $(self.get_ui_element_selector_by_role("pairwise_table_pecentage", true))
+        .off("change")
+        .on(
+          "change",
+          _.throttle((e) => {
+            self.show_percent_in_pairwise_table =
+              !self.show_percent_in_pairwise_table;
+            render_binned_table(
+              "attribute_table",
+              self.colorizer["category_map"],
+              self.colorizer["category_pairwise"]
+            );
+          }, 250)
+        );
+    }
+
+    if (kGlobals.network.GraphAttrbuteID in json) {
+      attributes = json[kGlobals.network.GraphAttrbuteID];
+    } else if (attributes && "hivtrace" in attributes) {
+      attributes = attributes["hivtrace"];
+    }
+
+    // Initialize class attributes
+    singletons = graph_data.Nodes.filter((v, i) => v.cluster === null).length;
+
+    self.nodes_by_cluster = {};
+
+    self.nodes = graph_data.Nodes.filter((v, i) => {
+      if (
+        v.cluster &&
+        typeof self.exclude_cluster_ids[v.cluster] === "undefined"
+      ) {
+        if (v.cluster in self.nodes_by_cluster) {
+          self.nodes_by_cluster[v.cluster].push(v);
+        } else {
+          self.nodes_by_cluster[v.cluster] = [v];
+        }
+
+        connected_links[i] = total++;
+        return true;
+      }
+      return false;
+    });
+
+    self.edges = graph_data.Edges.filter(
+      (v, i) => v.source in connected_links && v.target in connected_links
+    );
+
+    self.edges = self.edges.map((v, i) => {
+      var cp_v = _.clone(v);
+      cp_v.source = connected_links[v.source];
+      cp_v.target = connected_links[v.target];
+      cp_v.id = i;
+      return cp_v;
+    });
+
+    compute_node_degrees(self.nodes, self.edges);
+
+    default_layout(self.initial_packed);
+    self.clusters.forEach((d, i) => {
+      self.cluster_mapping[d.cluster_id] = i;
+      d.hxb2_linked = d.children.some((c) => c.hxb2_linked);
+      _compute_cluster_degrees(d);
+      d.distances = [];
+    });
+
+    try {
+      if (options && options["extra_menu"]) {
+        var extra_ui_container = d3.select(
+          self.get_ui_element_selector_by_role("extra_operations_container")
+        );
+
+        d3.select(
+          self.get_ui_element_selector_by_role("extra_operations_enclosure")
+        )
+          .selectAll("button")
+          .text(options["extra_menu"]["title"])
+          .append("span")
+          .classed("caret", "true");
+        //extra_ui_container
+        extra_ui_container.selectAll("li").remove();
+
+        options["extra_menu"]["items"].forEach(function (item, index) {
+          //console.log (item);
+          var handler_callback = item[1];
+          if (_.isFunction(item[0])) {
+            item[0](self, this.append("li"));
+          } else {
+            this.append("li")
+              .append("a")
+              .text(item[0])
+              .attr("href", "#")
+              .on("click", function (e) {
+                handler_callback(self, this);
+                d3.event.preventDefault();
+              });
+          }
+        }, extra_ui_container);
+
+        d3.select(
+          self.get_ui_element_selector_by_role("extra_operations_enclosure")
+        ).style("display", null);
+      }
+    } catch (err) {
+      console.log(err);
+    }
+
+    self._aux_populate_category_menus = function () {
+      if (button_bar_ui) {
+        // decide if the variable can be considered categorical by examining its range
+
+        //console.log ("self._aux_populate_category_menus");
+        var valid_cats = _.filter(
+          _.map(
+            graph_data[kGlobals.network.GraphAttrbuteID],
+            self._aux_populate_category_fields
+          ),
+          (d) =>
+            /*if (d.discrete) {
+              console.log (d["value_range"].length);
+          }*/
+            d.discrete &&
+            "value_range" in d &&
+            /*d["value_range"].length <= kGlobals.MaximumValuesInCategories &&*/
+            !d["_hidden_"]
+        );
+
+        var valid_shapes = _.filter(
+          valid_cats,
+          (d) =>
+            (d.discrete && d.dimension <= 7) ||
+            (d["raw_attribute_key"] in self.networkShapeScheme &&
+              !d["_hidden_"])
+        );
+
+        // sort values alphabetically for consistent coloring
+
+        _.each([valid_cats, valid_shapes], (list) => {
+          _.each(list, self._aux_process_category_values);
+        });
+
+        const colorStopsPath = [
+          kGlobals.network.GraphAttrbuteID,
+          self.colorizer["category_id"],
+          "color_stops",
+        ];
+
+        const color_stops = _.get(
+          graph_data,
+          colorStopsPath,
+          kGlobals.network.ContinuousColorStops
+        );
+
+        var valid_scales = _.filter(
+          _.map(graph_data[kGlobals.network.GraphAttrbuteID], (d, k) => {
+            function determine_scaling(d, values, scales) {
+              var low_var = Infinity;
+              _.each(scales, (scl, i) => {
+                d["value_range"] = d3.extent(values);
+                var bins = _.map(_.range(color_stops), () => 0);
+                scl.range([0, color_stops - 1]).domain(d["value_range"]);
+                _.each(values, (v) => {
+                  bins[Math.floor(scl(v))]++;
+                });
+
+                var mean = values.length / color_stops;
+                var vrnc = _.reduce(
+                  bins,
+                  (p, c) => p + (c - mean) * (c - mean)
+                );
+
+                if (vrnc < low_var) {
+                  low_var = vrnc;
+                  d["scale"] = scl;
+                }
+              });
+            }
+
+            d["raw_attribute_key"] = k;
+
+            if (d.type === "Number" || d.type === "Number-categories") {
+              var values = _.filter(
+                _.map(graph_data.Nodes, (nd) =>
+                  self.attribute_node_value_by_id(nd, k, d.type === "Number")
+                ),
+                (v) => _.isNumber(v)
+              );
+              // automatically determine the scale and see what spaces the values most evenly
+              const range = d3.extent(values);
+              const scales_to_consider = [d3.scale.linear()];
+              if (range[0] > 0) {
+                scales_to_consider.push(d3.scale.log());
+              }
+              if (range[0] >= 0) {
+                scales_to_consider.push(d3.scale.pow().exponent(1 / 3));
+                scales_to_consider.push(d3.scale.pow().exponent(1 / 4));
+                scales_to_consider.push(d3.scale.pow().exponent(1 / 2));
+                scales_to_consider.push(d3.scale.pow().exponent(1 / 8));
+                scales_to_consider.push(d3.scale.pow().exponent(1 / 16));
+              }
+              determine_scaling(d, values, scales_to_consider);
+            } else if (d.type === "Date") {
+              values = _.filter(
+                _.map(graph_data.Nodes, (nd) => {
+                  try {
+                    var a_date = self.attribute_node_value_by_id(nd, k);
+                    if (d.raw_attribute_key === "hiv_aids_dx_dt") {
+                      //console.log (nd, k, a_date);
+                    }
+                    HTX.HIVTxNetwork.inject_attribute_node_value_by_id(
+                      nd,
+                      k,
+                      this.parse_dates(a_date)
+                    );
+                  } catch (err) {
+                    HTX.HIVTxNetwork.inject_attribute_node_value_by_id(
+                      nd,
+                      k,
+                      kGlobals.missing.label
+                    );
+                  }
+                  return self.attribute_node_value_by_id(nd, k);
+                }),
+                (v) => (v === kGlobals.missing.label ? null : v)
+              );
+              // automatically determine the scale and see what spaces the values most evenly
+              if (values.length === 0) {
+                // invalid scale
+                return {};
+              }
+
+              determine_scaling(d, values, [d3.time.scale()]);
+            }
+            return d;
+          }),
+          (d) =>
+            (d.type === "Number" ||
+              d.type === "Date" ||
+              d.type === "Number-categories") &&
+            !d["_hidden_"]
+        );
+
+        const _menu_label_gen = (d) =>
+          (d["annotation"] ? "[" + d["annotation"] + "] " : "") + d["label"];
+
+        //console.log (valid_scales);
+        //valid_cats.splice (0,0, {'label' : 'None', 'index' : -1});
+
+        [
+          d3.select(self.get_ui_element_selector_by_role("attributes")),
+          d3.select(
+            self.get_ui_element_selector_by_role("attributes_cat", true)
+          ),
+        ].forEach((m) => {
+          //console.log (m);
+
+          if (m.empty()) {
+            return;
+          }
+          m.selectAll("li").remove();
+
+          var menu_items = [
+            [
+              [
+                "None",
+                null,
+                _.partial(self.handle_attribute_categorical, null),
+              ],
+            ],
+            [[__("network_tab")["categorical"], "heading", null]],
+          ].concat(
+            valid_cats.map((d, i) => [
+              [
+                _menu_label_gen(d),
+                d["raw_attribute_key"],
+                _.partial(
+                  self.handle_attribute_categorical,
+                  d["raw_attribute_key"]
+                ),
+              ],
+            ])
+          );
+
+          if (valid_scales.length) {
+            menu_items = menu_items
+              .concat([[[__("network_tab")["continuous"], "heading", null]]])
+              .concat(
+                valid_scales.map((d, i) => [
+                  [
+                    _menu_label_gen(d),
+                    d["raw_attribute_key"],
+                    _.partial(
+                      self.handle_attribute_continuous,
+                      d["raw_attribute_key"]
+                    ),
+                  ],
+                ])
+              );
+          }
+
+          var cat_menu = m.selectAll("li").data(menu_items);
+
+          cat_menu
+            .enter()
+            .append("li")
+            .classed("disabled", (d) => d[0][1] === "heading")
+            .style("font-variant", (d) =>
+              d[0][1] < -1 ? "small-caps" : "normal"
+            );
+
+          cat_menu
+            .selectAll("a")
+            .data((d) => d)
+            .enter()
+            .append("a")
+            .html((d, i, j) => {
+              let htm = d[0];
+              let type = "unknown";
+
+              if (d[1] in self.schema) {
+                type = self.schema[d[1]].type;
+              }
+
+              if (d[1] in self.uniqs && type === "String") {
+                htm =
+                  htm +
+                  '<span title="Number of unique values" class="badge pull-right">' +
+                  self.uniqs[d[1]] +
+                  "</span>";
+              }
+
+              return htm;
+            })
+            .attr("style", (d, i, j) => {
+              if (d[1] === "heading") return "font-style: italic";
+              if (j === 0) {
+                return " font-weight: bold;";
+              }
+              return null;
+            })
+            .attr("href", "#")
+            .on("click", (d) => {
+              if (d[2]) {
+                d[2].call();
+              }
+            });
+        });
+
+        [d3.select(self.get_ui_element_selector_by_role("shapes"))].forEach(
+          (m) => {
+            m.selectAll("li").remove();
+            var cat_menu = m
+              .selectAll("li")
+              .data(
+                [
+                  [
+                    [
+                      "None",
+                      null,
+                      _.partial(self.handle_shape_categorical, null),
+                    ],
+                  ],
+                ].concat(
+                  valid_shapes.map((d, i) => [
+                    [
+                      _menu_label_gen(d),
+                      d["raw_attribute_key"],
+                      _.partial(
+                        self.handle_shape_categorical,
+                        d["raw_attribute_key"]
+                      ),
+                    ],
+                  ])
+                )
+              );
+
+            cat_menu
+              .enter()
+              .append("li")
+              .style("font-variant", (d) =>
+                d[0][1] < -1 ? "small-caps" : "normal"
+              );
+
+            cat_menu
+              .selectAll("a")
+              .data((d) => d)
+              .enter()
+              .append("a")
+              .html((d, i, j) => {
+                let htm = d[0];
+                let type = "unknown";
+
+                if (_.contains(_.keys(self.schema), d[1])) {
+                  type = self.schema[d[1]].type;
+                }
+
+                if (_.contains(_.keys(self.uniqs), d[1]) && type === "String") {
+                  htm =
+                    htm +
+                    '<span title="Number of unique values" class="badge pull-right">' +
+                    self.uniqs[d[1]] +
+                    "</span>";
+                }
+
+                return htm;
+              })
+              .attr("style", (d, i, j) => {
+                if (j === 0) {
+                  return " font-weight: bold;";
+                }
+                return null;
+              })
+              .attr("href", "#")
+              .on("click", (d) => {
+                if (d[2]) {
+                  d[2].call();
+                }
+              });
+          }
+        );
+
+        $(self.get_ui_element_selector_by_role("opacity_invert"))
+          .off("click")
+          .on("click", function (e) {
+            if (self.colorizer["opacity_scale"]) {
+              self.colorizer["opacity_scale"].range(
+                self.colorizer["opacity_scale"].range().reverse()
+              );
+              self.update(true);
+              self.draw_attribute_labels();
+            }
+            $(this).toggleClass("btn-active btn-default");
+          });
+
+        $(self.get_ui_element_selector_by_role("attributes_invert"))
+          .off("click")
+          .on("click", function (e) {
+            if (self.colorizer["category_id"]) {
+              graph_data[kGlobals.network.GraphAttrbuteID][
+                self.colorizer["category_id"]
+              ]["scale"].range(
+                graph_data[kGlobals.network.GraphAttrbuteID][
+                  self.colorizer["category_id"]
+                ]["scale"]
+                  .range()
+                  .reverse()
+              );
+              self.clusters.forEach((the_cluster) => {
+                the_cluster["gradient"] = compute_cluster_gradient(
+                  the_cluster,
+                  self.colorizer["category_id"]
+                );
+              });
+              self.update(true);
+              self.draw_attribute_labels();
+            }
+            $(this).toggleClass("btn-active btn-default");
+          });
+
+        [d3.select(self.get_ui_element_selector_by_role("opacity"))].forEach(
+          (m) => {
+            m.selectAll("li").remove();
+            var cat_menu = m
+              .selectAll("li")
+              .data(
+                [
+                  [
+                    [
+                      "None",
+                      null,
+                      _.partial(self.handle_attribute_opacity, null),
+                    ],
+                  ],
+                ].concat(
+                  valid_scales.map((d, i) => [
+                    [
+                      d["label"],
+                      d["raw_attribute_key"],
+                      _.partial(
+                        self.handle_attribute_opacity,
+                        d["raw_attribute_key"]
+                      ),
+                    ],
+                  ])
+                )
+              );
+
+            cat_menu
+              .enter()
+              .append("li")
+              .style("font-variant", (d) =>
+                d[0][1] < -1 ? "small-caps" : "normal"
+              );
+            cat_menu
+              .selectAll("a")
+              .data((d) => d)
+              .enter()
+              .append("a")
+              .text((d, i, j) => d[0])
+              .attr("style", (d, i, j) => {
+                if (j === 0) {
+                  return " font-weight: bold;";
+                }
+                return null;
+              })
+              .attr("href", "#")
+              .on("click", (d) => {
+                if (d[2]) {
+                  d[2].call();
+                }
+              });
+          }
+        );
+      }
+    };
+
+    if (attributes) {
+      /*
+         map attributes into nodes and into the graph object itself using
+         kGlobals.network.GraphAttrbuteID as the key
+      */
+
+      if ("attribute_map" in attributes) {
+        var attribute_map = attributes["attribute_map"];
+
+        if ("map" in attribute_map && attribute_map["map"].length > 0) {
+          graph_data[kGlobals.network.GraphAttrbuteID] = attribute_map[
+            "map"
+          ].map((a, i) => ({
+            label: a,
+            type: null,
+            values: {},
+            index: i,
+            range: 0,
+          }));
+
+          graph_data.Nodes.forEach((n) => {
+            n[kGlobals.network.GraphAttrbuteID] = n.id.split(
+              attribute_map["delimiter"]
+            );
+            n[kGlobals.network.GraphAttrbuteID].forEach((v, i) => {
+              if (i < graph_data[kGlobals.network.GraphAttrbuteID].length) {
+                if (
+                  !(
+                    v in
+                    graph_data[kGlobals.network.GraphAttrbuteID][i]["values"]
+                  )
+                ) {
+                  graph_data[kGlobals.network.GraphAttrbuteID][i]["values"][v] =
+                    graph_data[kGlobals.network.GraphAttrbuteID][i]["range"];
+                  graph_data[kGlobals.network.GraphAttrbuteID][i]["range"] += 1;
+                }
+              }
+              //graph_data [kGlobals.network.GraphAttrbuteID][i]["values"][v] = 1 + (graph_data [kGlobals.network.GraphAttrbuteID][i]["values"][v] ? graph_data [kGlobals.network.GraphAttrbuteID][i]["values"][v] : 0);
+            });
+          });
+
+          graph_data[kGlobals.network.GraphAttrbuteID].forEach((d) => {
+            if (
+              d["range"] < graph_data.Nodes.length &&
+              d["range"] > 1 &&
+              d["range"] <= 20
+            ) {
+              d["type"] = "category";
+            }
+          });
+        }
+      }
+
+      for (const [key, def] of Object.entries(
+        self._networkPredefinedAttributeTransforms
+      )) {
+        self.populate_predefined_attribute(def, key);
+      }
+
+      self._aux_populate_category_menus();
+
+      // populate the UI elements
+    }
+
+    if (self.cluster_sizes.length > max_points_to_render) {
+      var sorted_array = self.cluster_sizes
+        .map((d, i) => [d, i + 1])
+        .sort((a, b) => a[0] - b[0]);
+
+      for (var k = 0; k < sorted_array.length - max_points_to_render; k++) {
+        self.exclude_cluster_ids[sorted_array[k][1]] = 1;
+      }
+
+      self.warning_string +=
+        (self.warning_string.length ? "<br>" : "") +
+        "Excluded " +
+        (sorted_array.length - max_points_to_render) +
+        " clusters (maximum size " +
+        sorted_array[k - 1][0] +
+        " nodes) because only " +
+        max_points_to_render +
+        " objects can be shown at once.";
+    }
+
+    self.edges.forEach((e, i) => {
+      self.clusters[
+        self.cluster_mapping[self.nodes[e.target].cluster]
+      ].distances.push(e.length);
+    });
+
+    self.clusters.forEach((d, i) => {
+      d.distances = helpers.describe_vector(d.distances);
+    });
+    //self.clusters
+
+    self.update();
+  }
+
+  function _cluster_table_draw_id(element, payload) {
+    var this_cell = d3.select(element);
+    this_cell.selectAll("*").remove();
+    const _is_subcluster = payload[1];
+    var cluster_id = payload[0];
+
+    if (_is_subcluster) {
+      //console.log (payload);
+
+      //this_cell.append("i")
+      //      .classed("fa fa-arrow-circle-o-right", true).style("padding-right", "0.25em");
+
+      /*if (payload[2].rr_count) {
+        this_cell
+          .append("i")
+          .classed("fa fa-exclamation-triangle", true)
+          .attr("title", "Subcluster has recent/rapid nodes");
+      }*/
+      this_cell.append("span").text(cluster_id).style("padding-right", "0.5em");
+
+      this_cell
+        .append("button")
+        .classed("btn btn-sm pull-right", true)
+        //.text(__("clusters_tab")["view"])
+        .on("click", (e) => {
+          self.view_subcluster(payload[2]);
+        })
+        .append("i")
+        .classed("fa fa-eye", true)
+        .attr("title", __("clusters_tab")["view"]);
+    } else {
+      this_cell.append("span").text(cluster_id).style("padding-right", "0.5em");
+      this_cell
+        .append("button")
+        .classed("btn btn-sm pull-right", true)
+        .style("margin-right", "0.25em")
+        .on("click", (e) => {
+          self.open_exclusive_tab_view(cluster_id);
+        })
+        .append("i")
+        .classed("fa fa-eye", true)
+        .attr("title", __("clusters_tab")["view"]);
+    }
+    this_cell
+      .append("button")
+      .classed("btn btn-sm pull-right", true)
+      .style("margin-right", "0.25em")
+      //.text(__("clusters_tab")["list"])
+      .attr("data-toggle", "modal")
+      .attr(
+        "data-target",
+        self.get_ui_element_selector_by_role("cluster_list", true)
+      )
+      .attr("data-cluster", cluster_id)
+      .append("i")
+      .classed("fa fa-list", true)
+      .attr("title", __("clusters_tab")["list"]);
+  }
+
+  function _cluster_table_draw_buttons(element, payload) {
+    var this_cell = d3.select(element);
+    const label_diff = function (c_info) {
+      const d = c_info["delta"];
+      const moved = c_info["moved"];
+      const deleted = c_info["deleted"];
+      const new_count = c_info["new_nodes"] ? c_info["new_nodes"] : 0;
+
+      let label_str = "";
+      if (moved) label_str = " " + moved + " moved ";
+      if (new_count) label_str += "+" + new_count + " new ";
+      if (deleted) label_str += "-" + deleted + " previous ";
+      return label_str;
+    };
+
+    var labels = [];
+
+    if (payload[4]) {
+      if (payload[4]["type"] === "new") {
+        if (payload[4]["moved"]) {
+          labels.push(["renamed " + label_diff(payload[4]), 2]);
+        } else {
+          labels.push(["new", 3]);
+        }
+      } else if (payload[4]["type"] === "extended") {
+        labels.push([label_diff(payload[4]), payload["4"]["flag"]]);
+      } else if (payload[4]["type"] === "merged") {
+        labels.push([
+          "Merged " +
+            payload[4]["old_clusters"].join(", ") +
+            " " +
+            label_diff(payload[4]),
+          payload["4"]["flag"],
+        ]);
+      }
+    }
+
+    labels.push([
+      [
+        payload[0]
+          ? __("clusters_tab")["expand"]
+          : __("clusters_tab")["collapse"],
+        payload[0] ? "fa-expand" : "fa-compress",
+      ],
+      0,
+    ]);
+    if (payload[1]) {
+      labels.push([["problematic", "fa-exclamation-circle"], 1]);
+    }
+    if (payload[2]) {
+      labels.push([["match", "fa-check-square"], 1]);
+    }
+    var buttons = this_cell.selectAll("button").data(labels);
+    buttons.enter().append("button");
+    buttons.exit().remove();
+    buttons
+      .classed("btn btn-xs", true)
+      .classed("btn-default", (d) => d[1] !== 1 && d[1] !== 2)
+      .classed("btn-danger", (d) => d[1] === 2)
+      .classed("btn-success", (d) => d[1] === 3)
+      /*.text(function(d) {
+        return d[0];
+      })*/
+      .style("margin-right", "0.25em")
+      .attr("disabled", (d) => (d[1] === 1 ? "disabled" : null))
+      .on("click", (d) => {
+        if (d[1] === 0) {
+          if (payload[0]) {
+            expand_cluster(self.clusters[payload[3] - 1], true);
+          } else {
+            collapse_cluster(self.clusters[payload[3] - 1]);
+          }
+          self.update_volatile_elements(self.cluster_table);
+          if (self.subcluster_table) {
+            self.update_volatile_elements(self.subcluster_table);
+          }
+        } else if (d[1] === 2 || d[1] === 3) {
+          //_social_view_options (labeled_links, shown_types),
+
+          var shown_types = { Existing: 1, "Newly added": 1 },
+            link_class = ["Existing", "Newly added"];
+
+          self
+            .open_exclusive_tab_view(
+              payload[3],
+              null,
+              (cluster_id) => "Cluster " + cluster_id + " [changes view]",
+              self._social_view_options(link_class, shown_types, (e) => {
+                if (
+                  _.isObject(e.source) &&
+                  HTX.HIVTxNetwork.is_new_node(e.source)
+                )
+                  return "Newly added";
+                if (
+                  _.isObject(e.target) &&
+                  HTX.HIVTxNetwork.is_new_node(e.target)
+                )
+                  return "Newly added";
+
+                return e.attributes.indexOf("added-to-prior") >= 0
+                  ? "Newly added"
+                  : "Existing";
+              })
+            )
+            .handle_attribute_categorical("_newly_added");
+        }
+      });
+    buttons.each(function (d, i) {
+      var this_e = d3.select(this);
+      if (_.isString(d[0])) {
+        this_e.selectAll("i").remove();
+        this_e.text(d[0]);
+      } else {
+        var i_span = this_e.selectAll("i").data([d[0]]);
+        i_span.enter().append("i");
+        i_span
+          .attr("class", (d) => "fa " + d[1], true)
+          .attr("title", (d) => d[0]);
+      }
+    });
+  }
+
+  function _node_table_draw_buttons(element, payload) {
+    var this_cell = d3.select(element);
+    let labels;
+    if (payload.length === 1) {
+      if (_.isString(payload[0])) {
+        labels = [[payload[0], 1, "btn-warning"]];
+      } else {
+        labels = ["can't be shown", 1];
+      }
+    } else {
+      labels = [[payload[0] ? "hide" : "show", 0]];
+      // TODO: deprecated? remove if not needed (5/22/2024 meeting with @spond, @daniel-ji, @stevenweaver)
+    }
+
+    if (payload.length === 2 && payload[1] >= 1) {
+      labels.push([
+        "view cluster",
+        function () {
+          self.open_exclusive_tab_view(payload[1]);
+        },
+      ]);
+    }
+
+    var buttons = this_cell.selectAll("button").data(labels);
+    buttons.enter().append("button");
+    buttons.exit().remove();
+    buttons
+      .classed("btn btn-xs btn-node-property", true)
+      .classed("btn-primary", true)
+      //.classed(function (d) {return d.length >=3 ? d[2] : "";}, function (d) {return d.length >= 3;})
+      .text((d) => d[0])
+      .attr("disabled", (d) =>
+        d[1] && !_.isFunction(d[1]) ? "disabled" : null
+      )
+      .on("click", (d) => {
+        if (_.isFunction(d[1])) {
+          d[1].call(d);
+        } else if (d[1] === 0) {
+          if (payload[0]) {
+            collapse_cluster(self.clusters[payload[3] - 1], true);
+          } else {
+            expand_cluster(self.clusters[payload[3] - 1]);
+          }
+          //format_a_cell(d3.select(element).datum(), null, element);
+          self.update_volatile_elements(nodesTab.getNodeTable());
+        }
+      });
+    buttons.each(function (d, e) {
+      if (d.length >= 3) {
+        d3.select(this).classed("btn-primary", false).classed(d[2], true);
+      }
+    });
+  }
+
+  /*self.process_table_volatile_event = function (e) {
+    console.log (e);
+    e.detail
+      .selectAll("td")
+      .filter(function(d) {
+        return "volatile" in d;
+      })
+      .each(function(d, i) {
+        format_a_cell(d, i, this);
+      });
+  };*/
+
+  self.update_volatile_elements = function (container) {
+    //var event = new CustomEvent('hiv-trace-viz-volatile-update', { detail: container });
+    //container.node().dispatchEvent (event);
+
+    container
+      .selectAll("td, th")
+      .filter((d) => "volatile" in d)
+      .each(function (d, i) {
+        // TODO: QUESTION: Should this have priority_set_editor arg passed in as well?
+        tables.format_a_cell(d, i, this);
+      });
+  };
+
+  self.redraw_tables = function () {
+    self.update_volatile_elements(self.cluster_table);
+    if (self.subcluster_table) {
+      self.update_volatile_elements(self.subcluster_table);
+    }
+    self.update_volatile_elements(nodesTab.getNodeTable());
+    if (self.priority_set_table) {
+      self.update_volatile_elements(self.priority_set_table);
+    }
+  };
+
+  self.draw_extended_node_table = function (
+    node_list,
+    container,
+    extra_columns
+  ) {
+    container = container || nodesTab.getNodeTable();
+
+    if (container) {
+      node_list = node_list || self.nodes;
+      var column_ids = self._extract_exportable_attributes(true);
+
+      self.displayed_node_subset = _.filter(
+        _.map(self.displayed_node_subset, (n, i) => {
+          if (_.isString(n)) {
+            n = _.find(column_ids, (cd) => cd.raw_attribute_key === n);
+
+            if (n) {
+              return n;
+            }
+            return column_ids[i];
+          }
+          return n;
+        }),
+        (c) => c
+      );
+
+      var node_data = self._extract_attributes_for_nodes(
+        node_list,
+        self.displayed_node_subset
+      );
+      node_data.splice(0, 1);
+      var table_headers = _.map(self.displayed_node_subset, (n, col_id) => ({
+        value: n.raw_attribute_key,
+        sort: "value",
+        filter: true,
+        volatile: true,
+        help: "label" in n ? n.label : n.raw_attribute_key,
+        //format: (d) => "label" in d ? d.label : d.raw_attribute_key,
+        callback: function (element, payload) {
+          var dropdown = d3
+            .select(element)
+            .append("div")
+            .classed("dropdown", true);
+          // add col_id to ensure that the dropdowns are unique
+          var menu_id = "hivtrace_node_column_" + payload + "_" + col_id;
+          var dropdown_button = dropdown
+            .append("button")
+            .classed({
+              btn: true,
+              "btn-default": true,
+              "btn-xs": true,
+              "dropdown-toggle": true,
+            })
+            .attr("type", "button")
+            .attr("data-toggle", "dropdown")
+            .attr("aria-haspopup", "true")
+            .attr("aria-expanded", "false")
+            .attr("id", menu_id);
+
+          function format_key(key) {
+            const formattedKey = jsConvert.toHeaderCase(key);
+            const words = formattedKey.split(" ");
+            const mappedWords = _.map(words, (word) => {
+              if (word.toLowerCase() === "hivtrace") {
+                return "HIV-TRACE";
+              }
+              if (word.toLowerCase() === "id") {
+                return "ID";
+              }
+
+              return word;
+            });
+            return mappedWords.join(" ");
+          }
+
+          function get_text_label(key) {
+            return key in json.patient_attribute_schema
+              ? json.patient_attribute_schema[key].label
+              : format_key(key);
+          }
+
+          dropdown_button.text(get_text_label(payload));
+
+          dropdown_button.append("i").classed({
+            fa: true,
+            "fa-caret-down": true,
+            "fa-lg": true,
+          });
+          var dropdown_list = dropdown
+            .append("ul")
+            .classed("dropdown-menu", true)
+            .attr("aria-labelledby", menu_id);
+
+          dropdown_list = dropdown_list
+            .selectAll("li")
+            .data(
+              _.filter(
+                column_ids,
+                (alt) => alt.raw_attribute_key !== n.raw_attribute_key
+              )
+            );
+          dropdown_list.enter().append("li");
+          dropdown_list.each(function (data, i) {
+            var handle_change = d3
+              .select(this)
+              .append("a")
+              .attr("href", "#")
+              .text((data) => get_text_label(data.raw_attribute_key));
+            handle_change.on("click", (d) => {
+              self.displayed_node_subset[col_id] = d;
+              self.draw_extended_node_table(
+                node_list,
+                container,
+                extra_columns
+              );
+            });
+          });
+          return dropdown;
+        },
+      }));
+
+      if (extra_columns) {
+        _.each(extra_columns, (d) => {
+          if (d.prepend) {
+            table_headers.splice(0, 0, d.description);
+          } else {
+            table_headers.push(d.description);
+          }
+        });
+      }
+      //console.log (self.displayed_node_subset);
+
+      var table_rows = node_data.map((n, i) => {
+        var this_row = _.map(n, (cell, c) => {
+          let cell_definition = null;
+
+          if (self.displayed_node_subset[c].type === "Date") {
+            cell_definition = {
+              value: cell,
+              format: function (v) {
+                if (v === kGlobals.missing.label) {
+                  return v;
+                }
+                return timeDateUtil.DateViewFormatSlider(v);
+              },
+            };
+          } else if (self.displayed_node_subset[c].type === "Number") {
+            cell_definition = { value: cell, format: d3.format(".2f") };
+          }
+          if (!cell_definition) {
+            cell_definition = { value: cell };
+          }
+
+          // this makes the table rendering too slow
+
+          /*if (c === 0 && self._is_CDC_) {
+             cell_definition.volatile = true;
+             cell_definition.actions = function (item, value) {
+              if (!clustersOfInterest.get_editor()) {
+                    return null;
+              } else {
+                    return [
+                        {
+                            "icon"   : "fa-plus-square",
+                            "action" : function (button,v) {
+                                if (clustersOfInterest.get_editor()) {
+                                    clustersOfInterest.get_editor().append_node_objects (d.children);
+                                }
+                                return false;
+                            },
+                            "help"   : "Add to priority set"
+                        }
+                    ];
+                }
+            };
+          }*/
+
+          return cell_definition;
+        });
+
+        if (extra_columns) {
+          _.each(extra_columns, (ed) => {
+            if (ed.prepend) {
+              this_row.splice(0, 0, ed.generator(node_list[i], self));
+            } else {
+              this_row.push(ed.generator(node_list[i], self));
+            }
+          });
+        }
+
+        return this_row;
+      });
+
+      self.draw_node_table(
+        null,
+        null,
+        [table_headers],
+        table_rows,
+        container,
+        'Showing <span class="badge" data-hivtrace-ui-role="table-count-shown">--</span>/<span class="badge" data-hivtrace-ui-role="table-count-total">--</span> network nodes'
+      );
+    }
+  };
+
+  self.generate_coi_temporal_report = function (ref_set, D) {
+    if (!ref_set) return {};
+    D = D || 0.005;
+
+    const nodesD = hivtrace_cluster_depthwise_traversal(
+      json["Nodes"],
+      json["Edges"],
+      (e) => e.length <= D,
+      null,
+      ref_set.node_objects
+    );
+
+    const full_subclusters = _.map(nodesD, (cc) =>
+      self.extract_single_cluster(cc, (e) => e.length <= D)
+    );
+    // the nodes in full_subclusters are now shallow clones
+    // const nodeid2cc = _.chain(nodesD) // unused var
+    //   .map((cc, i) => _.map(cc, (n) => [n.id, i]))
+    //   .flatten(1)
+    //   .object()
+    //   .value();
+    // node id => index of its connected component in the full_subclusters array
+    const pg_nodes = new Set(_.map(ref_set.node_objects, (n) => n.id));
+    // set of node IDs in the CoI
+    const seed_nodes = _.map(full_subclusters, (fc) =>
+      _.filter(fc["Nodes"], (n) => pg_nodes.has(n.id))
+    );
+    // for each connected component, store the list of nodes that are both in the CC and the CoI
+    // these are shallow copies
+    _.each(seed_nodes, (sn) => _.each(sn, (n) => (n.visited = false)));
+
+    var beginning_of_time = timeDateUtil.getCurrentDate();
+    beginning_of_time.setFullYear(1900);
+
+    // unused var
+    // const nodesD2 = _.map(full_subclusters, (fc, i) => hivtrace_cluster_depthwise_traversal(
+    //   fc["Nodes"],
+    //   fc["Edges"],
+    //   (e) => (e.length <= D),
+    //   null,
+    //   seed_nodes[i]
+    // ));
+
+    const network_events = _.sortBy([...self.priority_groups_all_events()]);
+    network_events.reverse();
+    const info_by_event = {};
+
+    _.each(network_events, (DT) => {
+      const event_date = timeDateUtil.DateViewFormatSlider.parse(DT);
+      const event_date_m3y = timeDateUtil.DateViewFormatSlider.parse(DT);
+      event_date_m3y.setFullYear(event_date.getFullYear() - 3);
+      const event_date_m1y = timeDateUtil.DateViewFormatSlider.parse(DT);
+      event_date_m1y.setFullYear(event_date.getFullYear() - 1);
+      const n_filter = (n) =>
+        self.filter_by_date(
+          beginning_of_time,
+          timeDateUtil._networkCDCDateField,
+          event_date,
+          n
+        );
+      const n_filter3 = (n) =>
+        self.filter_by_date(
+          event_date_m3y,
+          timeDateUtil._networkCDCDateField,
+          event_date,
+          n
+        );
+      const n_filter1 = (n) =>
+        self.filter_by_date(
+          event_date_m1y,
+          timeDateUtil._networkCDCDateField,
+          event_date,
+          n
+        );
+
+      let nodesD2 = _.map(full_subclusters, (fc, i) => {
+        const white_list = new Set(
+          _.map(_.filter(fc["Nodes"], n_filter), (n) => n.id)
+        );
+        const cc_nodes = fc["Nodes"];
+        return hivtrace_cluster_depthwise_traversal(
+          cc_nodes,
+          fc["Edges"],
+          (e) =>
+            e.length <= D &&
+            n_filter3(cc_nodes[e.source]) &&
+            n_filter3(cc_nodes[e.target]),
+          null,
+          _.filter(seed_nodes[i], n_filter),
+          white_list
+        );
+      });
+
+      nodesD2 = _.flatten(nodesD2, 1);
+      //console.log (nodesD2);
+
+      info_by_event[DT] = {
+        connected_componets: _.map(nodesD2, (nd) => nd.length),
+        priority_nodes: _.map(nodesD2, (nd) =>
+          _.map(_.filter(nd, n_filter1), (n) => n.id)
+        ),
+      };
+
+      info_by_event[DT]["national_priority"] = _.map(
+        info_by_event[DT].priority_nodes,
+        (m) => m.length >= self.CDC_data["autocreate-priority-set-size"]
+      );
+    });
+
+    const report = {
+      node_info: _.map(ref_set.node_objects, (n) => [
+        n.id,
+        timeDateUtil.DateViewFormatSlider(
+          self.attribute_node_value_by_id(n, timeDateUtil._networkCDCDateField)
+        ),
+      ]),
+      event_info: info_by_event,
+    };
+
+    /*let options = ["0","1","2","3","4","5","6","7","8","9","10"];
+          let rename = {};
+          _.each (report.node_info, (n)=> {
+                rename[n[0]] = "N" + _.sample (options, 9).join ("");
+                n[0] = rename[n[0]];
+          });
+          _.each (report.event_info, (d)=> {
+              d.priority_nodes = _.map (d.priority_nodes, (d)=>_.map (d, (n)=>rename[n]));
+          });
+          //console.log (report);
+          */
+
+    //helpers.export_json_button(report);
+    return report;
+  };
+
+  self.draw_node_table = function (
+    extra_columns,
+    node_list,
+    headers,
+    rows,
+    container,
+    table_caption
+  ) {
+    container = container || nodesTab.getNodeTable();
+
+    if (container) {
+      node_list = node_list || self.nodes;
+
+      if (!headers) {
+        headers = [
+          [
+            {
+              value: "ID",
+              sort: "value",
+              help: "Node ID",
+            },
+            {
+              value: "Action",
+              sort: "value",
+            },
+            {
+              value: "# of links",
+              sort: "value",
+              help: "Number of links (Node degree)",
+            },
+            {
+              value: "Cluster",
+              sort: "value",
+              help: "Which cluster does the node belong to",
+            },
+          ],
+        ];
+
+        if (extra_columns) {
+          _.each(extra_columns, (d) => {
+            if (d.prepend) {
+              headers[0].splice(0, 0, d.description);
+            } else {
+              headers[0].push(d.description);
+            }
+          });
+        }
+
+        rows = node_list.map((n, i) => {
+          var this_row = [
+            {
+              value: n.id,
+              help: "Node ID",
+            },
+            {
+              value: function () {
+                if (n.node_class !== "injected") {
+                  try {
+                    if (self.exclude_cluster_ids[n.cluster]) {
+                      // parent cluster can't be rendered
+                      // because of size restrictions
+                      return [n.cluster];
+                    }
+                    return [
+                      !self.clusters[self.cluster_mapping[n.cluster]].collapsed,
+                      n.cluster,
+                    ];
+                  } catch (err) {
+                    return [-1];
+                  }
+                } else {
+                  return [n.node_annotation];
+                }
+              },
+              callback: _node_table_draw_buttons,
+              volatile: true,
+            },
+            {
+              value: "degree" in n ? n.degree : "Not defined",
+              help: "Node degree",
+            },
+            {
+              value: "cluster" in n ? n.cluster : "Not defined",
+              help: "Which cluster does the node belong to",
+            },
+          ];
+
+          if (extra_columns) {
+            _.each(extra_columns, (ed) => {
+              if (ed.prepend) {
+                this_row.splice(0, 0, ed.generator(n, self));
+              } else {
+                this_row.push(ed.generator(n, self));
+              }
+            });
+          }
+          return this_row;
+        });
+      }
+
+      tables.add_a_sortable_table(
+        container,
+        headers,
+        rows,
+        true,
+        table_caption,
+        clustersOfInterest.get_editor()
+        // rows
+      );
+    }
+  };
+
+  self.draw_cluster_table = function (extra_columns, element, options) {
+    var skip_clusters = options && options["no-clusters"];
+    var skip_subclusters = !(options && options["subclusters"]);
+
+    element = element || self.cluster_table;
+
+    if (element) {
+      var headers = [
+        [
+          {
+            value: __("general")["cluster"] + " ID",
+            sort: function (c) {
+              return _.map(
+                c.value[0].split(kGlobals.SubclusterSeparator),
+                (ss) => kGlobals.formats.DotFormatPadder(Number(ss))
+              ).join("|");
+            },
+            help: "Unique cluster ID",
+          },
+          {
+            value: __("general")["attributes"],
+            sort: function (c) {
+              c = c.value();
+              if (c[4]) {
+                // has attributes
+                return c[4]["delta"];
+              }
+              return c[0];
+            },
+            help: "Visibility in the network tab and other attributes",
+          },
+          {
+            value: __("clusters_tab")["size"],
+            sort: "value",
+            help: "Number of nodes in the cluster",
+          },
+        ],
+      ];
+
+      if (self.cluster_attributes) {
+        headers[0][1]["presort"] = "desc";
+      }
+
+      if (self._is_seguro) {
+        headers[0].push({
+          value: __("clusters_tab")["number_of_genotypes_in_past_2_months"],
+          sort: "value",
+          help: "# of cases in cluster genotyped in the last 2 months",
+        });
+
+        headers[0].push({
+          value:
+            __("clusters_tab")["scaled_number_of_genotypes_in_past_2_months"],
+          sort: "value",
+          help: "# of cases in cluster genotyped in the last 2 months divided by the square-root of the cluster size",
+        });
+      }
+
+      if (!self._is_CDC_) {
+        headers[0].push({
+          value:
+            __("statistics")["links_per_node"] +
+            "<br>" +
+            __("statistics")["mean"] +
+            "[" +
+            __("statistics")["median"] +
+            ", IQR]",
+          html: true,
+        });
+
+        headers[0].push({
+          value:
+            __("statistics")["genetic_distances_among_linked_nodes"] +
+            "<br>" +
+            __("statistics")["mean"] +
+            "[" +
+            __("statistics")["median"] +
+            ", IQR]",
+          help: "Genetic distance among nodes in the cluster",
+          html: true,
+        });
+      }
+
+      if (extra_columns) {
+        _.each(extra_columns, (d) => {
+          headers[0].push(d.description);
+        });
+      }
+
+      if (options && options["headers"]) {
+        options["headers"](headers);
+      }
+
+      var rows = [];
+
+      _.each(self.clusters, (cluster) => {
+        var make_row = function (d, is_subcluster) {
+          var this_row = [
+            {
+              value: [d.cluster_id, is_subcluster, d], //.cluster_id,
+              callback: _cluster_table_draw_id,
+            },
+            {
+              value: function () {
+                var actual_cluster = is_subcluster ? d.parent_cluster : d;
+
+                return [
+                  actual_cluster.collapsed,
+                  actual_cluster.hxb2_linked,
+                  actual_cluster.match_filter,
+                  actual_cluster.cluster_id,
+                  is_subcluster
+                    ? null
+                    : self.cluster_attributes
+                    ? self.cluster_attributes[actual_cluster.cluster_id]
+                    : null,
+                ];
+              },
+              callback: _cluster_table_draw_buttons,
+              volatile: true,
+            },
+            {
+              value: d.children.length,
+            },
+          ];
+
+          if (self._is_CDC_) {
+            this_row[2].volatile = true;
+            this_row[2].actions = function (item, value) {
+              if (!clustersOfInterest.get_editor()) {
+                return null;
+              }
+              return [
+                {
+                  icon: "fa-plus",
+                  action: function (button, v) {
+                    if (clustersOfInterest.get_editor()) {
+                      clustersOfInterest
+                        .get_editor()
+                        .append_node_objects(d.children);
+                    }
+                    return false;
+                  },
+                  help: "Add to cluster of interest",
+                },
+              ];
+            };
+          }
+
+          if (self._is_seguro) {
+            this_row.push({
+              value: d,
+              format: function (d) {
+                return _.filter(
+                  d.children,
+                  (child) =>
+                    d3.time.months(
+                      child.patient_attributes["sample_dt"],
+                      timeDateUtil.getCurrentDate()
+                    ).length <= 2
+                ).length;
+              },
+            });
+
+            this_row.push({
+              value: d,
+              format: function (d) {
+                const recent = _.filter(
+                  d.children,
+                  (child) =>
+                    d3.time.months(
+                      child.patient_attributes["sample_dt"],
+                      timeDateUtil.getCurrentDate()
+                    ).length <= 2
+                ).length;
+                return recent / Math.sqrt(d.children.length);
+              },
+            });
+          }
+
+          if (!self._is_CDC_) {
+            this_row.push({
+              value: d.degrees,
+              format: function (d) {
+                try {
+                  return (
+                    kGlobals.formats.FloatFormat(d["mean"]) +
+                    " [" +
+                    kGlobals.formats.FloatFormat(d["median"]) +
+                    ", " +
+                    kGlobals.formats.FloatFormat(d["Q1"]) +
+                    " - " +
+                    kGlobals.formats.FloatFormat(d["Q3"]) +
+                    "]"
+                  );
+                } catch (e) {
+                  return "";
+                }
+              },
+            });
+            this_row.push({
+              value: d.distances,
+              format: function (d) {
+                try {
+                  return (
+                    kGlobals.formats.FloatFormat(d["mean"]) +
+                    " [" +
+                    kGlobals.formats.FloatFormat(d["median"]) +
+                    ", " +
+                    kGlobals.formats.FloatFormat(d["Q1"]) +
+                    " - " +
+                    kGlobals.formats.FloatFormat(d["Q3"]) +
+                    "]"
+                  );
+                } catch (e) {
+                  return "";
+                }
+              },
+            });
+          }
+          if (extra_columns) {
+            _.each(extra_columns, (ed) => {
+              this_row.push(ed.generator(d, self));
+            });
+          }
+
+          return this_row;
+        };
+
+        if (!skip_clusters) {
+          rows.push(make_row(cluster, false));
+        }
+
+        if (!skip_subclusters) {
+          _.each(cluster.subclusters, (sub_cluster) => {
+            rows.push(make_row(sub_cluster, true));
+          });
+        }
+      });
+
+      tables.add_a_sortable_table(
+        element,
+        headers,
+        rows,
+        true,
+        options && options["caption"] ? options["caption"] : null,
+        clustersOfInterest.get_editor()
+      );
+    }
+  };
+
+  /*------------ Update layout code ---------------*/
+  function update_network_string(node_count, edge_count) {
+    if (network_status_string) {
+      const clusters_shown = _.filter(
+        self.clusters,
+        (c) => !c.collapsed
+      ).length;
+
+      const clusters_selected = _.filter(
+        self.clusters,
+        (c) =>
+          !c.is_hidden && c.match_filter !== undefined && c.match_filter > 0
+      ).length;
+
+      const nodes_selected = _.filter(
+        self.nodes,
+        (n) => n.match_filter && !n.is_hidden
+      ).length;
+
+      // const clusters_removed = self.cluster_sizes.length - self.clusters.length;
+      // const nodes_removed = graph_data.Nodes.length - singletons - self.nodes.length;
+      // const networkString = "Displaying a network on <strong>" + self.nodes.length + "</strong> nodes, <strong>" + self.clusters.length + "</strong> clusters"
+      //         + (clusters_removed > 0 ? " (an additional " + clusters_removed + " clusters and " + nodes_removed + " nodes have been removed due to network size constraints)" : "") + ". <strong>"
+      //         + clusters_shown +"</strong> clusters are expanded. Of <strong>" + self.edges.length + "</strong> edges, <strong>" + draw_me.edges.length + "</strong>, and of  <strong>" + self.nodes.length  + " </strong> nodes,  <strong>" + draw_me.nodes.length + " </strong> are displayed. ";
+      // if (singletons > 0) {
+      //   networkString += "<strong>" +singletons + "</strong> singleton nodes are not shown. ";
+      // }
+
+      const networkString =
+        "<span class = 'badge'>" +
+        self.clusters.length +
+        "</span> clusters <span class = 'label label-primary'>" +
+        clusters_shown +
+        " expanded / " +
+        clusters_selected +
+        " match </span> <span class = 'badge'> " +
+        self.nodes.length +
+        "</span> nodes <span class = 'label label-primary'>" +
+        node_count +
+        " shown / " +
+        nodes_selected +
+        " match </span> <span class = 'badge'> " +
+        self.edges.length +
+        "</span> " +
+        (self._is_CDC_ ? "links" : "edges") +
+        " <span class = 'label label-primary'>" +
+        edge_count +
+        " shown</span>";
+
+      d3.select(network_status_string).html(networkString);
+    }
+  }
+
+  function draw_a_node(container, node) {
+    if (node) {
+      container = d3.select(container);
+      //console.log (container.selectAll ("path"));
+      //var path_component = containter.selectAll ("path");
+
+      let symbol_type;
+
+      if (node.hxb2_linked && !node.is_lanl) {
+        symbol_type = "cross";
+      } else if (node.is_lanl) {
+        symbol_type = "triangle-down";
+      } else {
+        symbol_type = self.node_shaper["shaper"](node);
+      }
+
+      node.rendered_size = Math.sqrt(node_size(node)) / 2 + 2;
+
+      container
+        .selectAll("path")
+        .attr("d", misc.symbol(symbol_type).size(node_size(node)))
+        .style("fill", (d) => node_color(d));
+
+      if (node.show_label) {
+        if (container.selectAll("text").empty()) {
+          node.label_x = 0;
+          node.label_y = 0;
+          container
+            .append("text")
+            .classed("node-label", true)
+            .text(node.id)
+            .attr(
+              "transform",
+              "translate(" +
+                node.rendered_size * 1.25 +
+                "," +
+                node.rendered_size * 0.5 +
+                ")"
+            )
+            .datum(node)
+            .call(self.node_label_drag);
+        }
+      } else {
+        container.selectAll("text").remove();
+      }
+
+      container
+        //.attr("d", misc.symbol(symbol_type).size(node_size(node)))
+        .attr("class", "node")
+        .classed(
+          "selected_object",
+          (d) => d.match_filter && !self.hide_unselected
+        )
+        .classed("injected_object", (d) => d.node_class === "injected")
+        .attr("transform", (d) => "translate(" + d.x + "," + d.y + ")")
+        .style("opacity", (d) => node_opacity(d))
+        .style("display", (d) => {
+          if (d.is_hidden) return "none";
+          return null;
+        })
+        .call(
+          network_layout.drag().on("dragstart", (d) => {
+            d3.event.sourceEvent.stopPropagation();
+            node_pop_off();
+          })
+        )
+        .on("dragend", (d) => {
+          d3.event.sourceEvent.stopPropagation();
+        })
+        .on("click", handle_node_click)
+        .on("mouseover", node_pop_on)
+        .on("mouseout", node_pop_off);
+    }
+  }
+
+  function draw_a_cluster(container, the_cluster) {
+    var container_group = d3.select(container);
+
+    var draw_from = the_cluster["binned_attributes"]
+      ? the_cluster["binned_attributes"].map((d) => d.concat([0]))
+      : [[null, 1, 0]];
+
+    if (the_cluster.match_filter) {
+      draw_from = draw_from.concat([
+        ["selected", the_cluster.match_filter, 1],
+        [
+          "not selected",
+          the_cluster.children.length - the_cluster.match_filter,
+          1,
+        ],
+      ]);
+    }
+
+    var sums = [
+      d3.sum(
+        draw_from.filter((d) => d[2] === 0),
+        (d) => d[1]
+      ),
+      d3.sum(
+        draw_from.filter((d) => d[2] !== 0),
+        (d) => d[1]
+      ),
+    ];
+
+    var running_totals = [0, 0];
+
+    draw_from = draw_from.map((d) => {
+      var index = d[2];
+      var v = {
+        container: container,
+        cluster: the_cluster,
+        startAngle: (running_totals[index] / sums[index]) * 2 * Math.PI,
+        endAngle: ((running_totals[index] + d[1]) / sums[index]) * 2 * Math.PI,
+        name: d[0],
+        rim: index > 0,
+      };
+      running_totals[index] += d[1];
+      return v;
+    });
+
+    var arc_radius = cluster_box_size(the_cluster) * 0.5;
+    the_cluster.rendered_size = arc_radius + 2;
+    var paths = container_group.selectAll("path").data(draw_from);
+    paths.enter().append("path");
+    paths.exit().remove();
+
+    paths
+      .classed("cluster", true)
+      .classed(
+        "hiv-trace-problematic",
+        (d) => the_cluster.hxb2_linked && !d.rim
+      )
+      .classed("hiv-trace-selected", (d) => d.rim)
+      .attr("d", (d) =>
+        (d.rim
+          ? d3.svg
+              .arc()
+              .innerRadius(arc_radius + 2)
+              .outerRadius(arc_radius + 5)
+          : d3.svg.arc().innerRadius(0).outerRadius(arc_radius))(d)
+      )
+      .style("fill", (d, i) => {
+        if (d.rim) {
+          return self.colorizer["selected"](d.name);
+        }
+
+        if (the_cluster["gradient"]) {
+          return "url(#" + the_cluster["gradient"] + ")";
+        }
+
+        return cluster_color(the_cluster, d.name);
+      })
+      .style("stroke-linejoin", (d, i) => (draw_from.length > 1 ? "round" : ""))
+      .style("display", (d) => {
+        if (the_cluster.is_hidden) return "none";
+        return null;
+      });
+  }
+
+  function check_for_predefined_shapes(cat_id) {
+    //console.log (cat_id);
+
+    if (cat_id in self.networkShapeScheme) {
+      var domain = _.range(
+        0,
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["value_range"]
+          .length
+      );
+
+      return {
+        domain: domain,
+        range: _.map(
+          domain,
+          (v) =>
+            self.networkShapeScheme[cat_id][
+              graph_data[kGlobals.network.GraphAttrbuteID][cat_id][
+                "value_range"
+              ][v]
+            ]
+        ),
+      };
+    }
+    return {
+      domain: _.range(
+        0,
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id].dimension
+      ),
+      range: kGlobals.ShapeOrdering,
+    };
+  }
+
+  self.handle_shape_categorical = function (cat_id) {
+    var set_attr = "None";
+
+    ["shapes"].forEach((lbl) => {
+      d3.select(self.get_ui_element_selector_by_role(lbl))
+        .selectAll("li")
+        .selectAll("a")
+        .attr("style", (d, i) => {
+          if (d[1] === cat_id) {
+            set_attr = d[0];
+            return " font-weight: bold;";
+          }
+          return null;
+        });
+      d3.select(self.get_ui_element_selector_by_role(lbl + "_label")).html(
+        __("network_tab")["shape"] +
+          ": " +
+          set_attr +
+          ' <span class="caret"></span>'
+      );
+    });
+
+    if (cat_id) {
+      var domain_range = check_for_predefined_shapes(cat_id);
+
+      var shape_mapper = d3.scale
+        .ordinal()
+        .domain(domain_range["domain"])
+        .range(domain_range["range"]);
+      self.node_shaper["id"] = cat_id;
+      self.node_shaper["shaper"] = function (d) {
+        return shape_mapper(
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["value_map"](
+            self.attribute_node_value_by_id(d, cat_id)
+          )
+        );
+      };
+      self.node_shaper["category_map"] =
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["value_map"];
+    } else {
+      self.node_shaper.id = null;
+      self.node_shaper.shaper = () => "circle";
+      self.node_shaper["category_map"] = null;
+    }
+    //console.log (graph_data [kGlobals.network.GraphAttrbuteID][cat_id]['value_map'], self.node_shaper.domain(), self.node_shaper.range());
+    self.draw_attribute_labels();
+    self.update(true);
+    d3.event.preventDefault();
+  };
+
+  self.renderColorPicker = function (cat_id, type) {
+    const renderColorPickerCategorical = function (cat_id) {
+      // For each unique value, render item.
+      let colorizer = self.colorizer;
+      let items = _.map(_.filter(self.uniqValues[cat_id]), (d) =>
+        colorPicker.colorPickerInput(d, colorizer)
+      );
+
+      $("#colorPickerRow").html(items.join(""));
+
+      // Set onchange event for items
+      $(".hivtrace-color-picker").change((e) => {
+        let color = e.target.value;
+        let name = e.target.name;
+
+        // Set color in user-defined colorizer
+        if (
+          _.isUndefined(
+            graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"]
+          )
+        ) {
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"] =
+            {};
+        }
+
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"][
+          name
+        ] = color;
+        self.handle_attribute_categorical(cat_id);
+      });
+    };
+
+    const renderColorPickerContinuous = function (cat_id, color_stops) {
+      // For each unique value, render item.
+      // Min and max range for continuous values
+      let items = [
+        colorPicker.colorStops("Color Stops", color_stops),
+        colorPicker.colorPickerInputContinuous(
+          "Min",
+          self.uniqValues[cat_id]["min"]
+        ),
+        colorPicker.colorPickerInputContinuous(
+          "Max",
+          self.uniqValues[cat_id]["max"]
+        ),
+      ];
+
+      $("#colorPickerRow").html(items.join(""));
+
+      // Set onchange event for items
+      $(".hivtrace-color-picker").change((e) => {
+        let color = e.target.value;
+        let name = e.target.name;
+
+        // Set color in user-defined colorizer
+        if (
+          _.isUndefined(
+            graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"]
+          )
+        ) {
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"] =
+            {};
+        }
+
+        // get both for user-defined
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"][
+          name
+        ] = color;
+        self.handle_attribute_continuous(cat_id);
+      });
+
+      // Set onchange event for items
+      $(".hivtrace-color-stops").change((e) => {
+        let num = parseInt(e.target.value);
+        graph_data[kGlobals.network.GraphAttrbuteID][
+          self.colorizer["category_id"]
+        ]["color_stops"] = num;
+
+        self._aux_populate_category_menus();
+        self.handle_attribute_continuous(cat_id);
+        self.update();
+      });
+    };
+
+    if (type === "categorical") {
+      renderColorPickerCategorical(cat_id);
+    } else if (type === "continuous") {
+      renderColorPickerContinuous(
+        cat_id,
+        graph_data[kGlobals.network.GraphAttrbuteID][
+          self.colorizer["category_id"]
+        ]["color_stops"]
+      );
+    } else {
+      console.log("Error: type not recognized");
+    }
+
+    if (cat_id !== null) {
+      $("#colorPickerOption").show();
+    } else {
+      $("#colorPickerOption").hide();
+    }
+  };
+
+  self.draw_attribute_labels = function () {
+    // draw color legend in the network SVG
+
+    var determine_label_format_cont = function (field_data) {
+      if ("label_format" in field_data) {
+        return field_data["label_format"];
+      }
+      if (field_data["type"] === "Date") {
+        return timeDateUtil.DateViewFormatShort;
+      }
+      return d3.format(",.4r");
+    };
+
+    self.legend_svg.selectAll("g.hiv-trace-legend").remove();
+
+    var offset = 10;
+
+    if (self.legend_caption) {
+      self.legend_svg
+        .append("g")
+        .attr("transform", "translate(0," + offset + ")")
+        .classed("hiv-trace-legend", true)
+        .append("text")
+        .text(self.legend_caption)
+        .style("font-weight", "bold");
+      offset += 18;
+    }
+
+    if (self.edge_legend) {
+      self.legend_svg
+        .append("g")
+        .attr("transform", "translate(0," + offset + ")")
+        .classed("hiv-trace-legend", true)
+        .append("text")
+        .text(self.edge_legend["caption"])
+        .style("font-weight", "bold");
+      offset += 18;
+
+      _.each(self.edge_legend["types"], (value, key) => {
+        self.legend_svg
+          .append("g")
+          .classed("hiv-trace-legend", true)
+          .attr("transform", "translate(20," + offset + ")")
+          .append("text")
+          .text(key);
+
+        value.call(
+          self.legend_svg
+            .append("g")
+            .classed("hiv-trace-legend", true)
+            .attr("transform", "translate(0," + offset + ")")
+            .append("line")
+            .attr("x1", "0")
+            .attr("y1", "-4")
+            .attr("x2", "12")
+            .attr("y2", "-4")
+            .classed("legend", true)
+        );
+
+        offset += 18;
+      });
+    }
+
+    if (self.colorizer["category_id"]) {
+      //_.each (self.colorizer["category_map"](null, "map"), function (v){ console.log (v); });
+
+      self.legend_svg
+        .append("g")
+        .attr("transform", "translate(0," + offset + ")")
+        .classed("hiv-trace-legend", true)
+        .append("text")
+        .text(
+          "Color: " +
+            self.json[kGlobals.network.GraphAttrbuteID][
+              self.colorizer["category_id"]
+            ].label
+        )
+        .style("font-weight", "bold");
+      offset += 18;
+
+      if (self.colorizer["continuous"]) {
+        var anchor_format = determine_label_format_cont(
+          graph_data[kGlobals.network.GraphAttrbuteID][
+            self.colorizer["category_id"]
+          ]
+        );
+
+        var color_stops =
+          graph_data[kGlobals.network.GraphAttrbuteID][
+            self.colorizer["category_id"]
+          ]["color_stops"] || kGlobals.network.ContinuousColorStops;
+
+        var scale =
+          graph_data[kGlobals.network.GraphAttrbuteID][
+            self.colorizer["category_id"]
+          ]["scale"];
+
+        _.each(_.range(color_stops), (value) => {
+          var x = scale.invert(value);
+          self.legend_svg
+            .append("g")
+            .classed("hiv-trace-legend", true)
+            .attr("transform", "translate(20," + offset + ")")
+            .append("text")
+            .text(anchor_format(x));
+          self.legend_svg
+            .append("g")
+            .classed("hiv-trace-legend", true)
+            .attr("transform", "translate(0," + offset + ")")
+            .append("circle")
+            .attr("cx", "8")
+            .attr("cy", "-4")
+            .attr("r", "8")
+            .classed("legend", true)
+            .style("fill", self.colorizer["category"](x));
+          offset += 18;
+        });
+
+        if (
+          "category_values" in
+          graph_data[kGlobals.network.GraphAttrbuteID][
+            self.colorizer["category_id"]
+          ]
+        ) {
+          _.each(
+            graph_data[kGlobals.network.GraphAttrbuteID][
+              self.colorizer["category_id"]
+            ]["category_values"],
+            (value) => {
+              self.legend_svg
+                .append("g")
+                .classed("hiv-trace-legend", true)
+                .attr("transform", "translate(20," + offset + ")")
+                .append("text")
+                .text(value);
+              self.legend_svg
+                .append("g")
+                .classed("hiv-trace-legend", true)
+                .attr("transform", "translate(0," + offset + ")")
+                .append("circle")
+                .attr("cx", "8")
+                .attr("cy", "-4")
+                .attr("r", "8")
+                .classed("legend", true)
+                .style("fill", self.colorizer["category"](value));
+
+              offset += 18;
+            }
+          );
+        }
+
+        self.legend_svg
+          .append("g")
+          .classed("hiv-trace-legend", true)
+          .attr("transform", "translate(20," + offset + ")")
+          .append("text")
+          .text("missing");
+        self.legend_svg
+          .append("g")
+          .classed("hiv-trace-legend", true)
+          .attr("transform", "translate(0," + offset + ")")
+          .append("circle")
+          .attr("cx", "8")
+          .attr("cy", "-4")
+          .attr("r", "8")
+          .classed("legend", true)
+          .style("fill", kGlobals.missing.color);
+
+        offset += 18;
+      } else {
+        _.each(self.colorizer["category_map"](null, "map"), (value, key) => {
+          self.legend_svg
+            .append("g")
+            .classed("hiv-trace-legend", true)
+            .attr("transform", "translate(20," + offset + ")")
+            .append("text")
+            .text(key);
+          self.legend_svg
+            .append("g")
+            .classed("hiv-trace-legend", true)
+            .attr("transform", "translate(0," + offset + ")")
+            .append("circle")
+            .attr("cx", "8")
+            .attr("cy", "-4")
+            .attr("r", "8")
+            .classed("legend", true)
+            .style("fill", self.colorizer["category"](key));
+
+          offset += 18;
+        });
+      }
+    }
+
+    if (self.node_shaper["id"]) {
+      self.legend_svg
+        .append("g")
+        .attr("transform", "translate(0," + offset + ")")
+        .classed("hiv-trace-legend", true)
+        .append("text")
+        .text(
+          "Shape: " +
+            self.json[kGlobals.network.GraphAttrbuteID][self.node_shaper["id"]]
+              .label
+        )
+        .style("font-weight", "bold");
+      offset += 18;
+
+      var domain_range = check_for_predefined_shapes(self.node_shaper["id"]);
+      var shape_mapper = d3.scale
+        .ordinal()
+        .domain(domain_range["domain"])
+        .range(domain_range["range"]);
+
+      _.each(self.node_shaper["category_map"](null, "map"), (value, key) => {
+        self.legend_svg
+          .append("g")
+          .classed("hiv-trace-legend", true)
+          .attr("transform", "translate(20," + offset + ")")
+          .append("text")
+          .text(key);
+
+        self.legend_svg
+          .append("g")
+          .classed("hiv-trace-legend", true)
+          .attr("transform", "translate(0," + offset + ")")
+          .append("path")
+          .attr("transform", "translate(5,-5)")
+          .attr("d", misc.symbol(shape_mapper(value)).size(128))
+          .classed("legend", true)
+          .style("fill", "none");
+
+        offset += 18;
+      });
+    }
+
+    if (self.colorizer["opacity_id"]) {
+      self.legend_svg
+        .append("g")
+        .attr("transform", "translate(0," + offset + ")")
+        .classed("hiv-trace-legend", true)
+        .append("text")
+        .text(
+          __("network_tab")["opacity"] +
+            ": " +
+            self.json[kGlobals.network.GraphAttrbuteID][
+              self.colorizer["opacity_id"]
+            ].label
+        )
+        .style("font-weight", "bold");
+      offset += 18;
+
+      anchor_format = determine_label_format_cont(
+        graph_data[kGlobals.network.GraphAttrbuteID][
+          self.colorizer["opacity_id"]
+        ]
+      );
+
+      scale =
+        graph_data[kGlobals.network.GraphAttrbuteID][
+          self.colorizer["opacity_id"]
+        ]["scale"];
+
+      _.each(_.range(kGlobals.network.ContinuousColorStops), (value) => {
+        var x = scale.invert(value);
+        self.legend_svg
+          .append("g")
+          .classed("hiv-trace-legend", true)
+          .attr("transform", "translate(20," + offset + ")")
+          .append("text")
+          .text(anchor_format(x));
+        self.legend_svg
+          .append("g")
+          .classed("hiv-trace-legend", true)
+          .attr("transform", "translate(0," + offset + ")")
+          .append("circle")
+          .attr("cx", "8")
+          .attr("cy", "-4")
+          .attr("r", "8")
+          .classed("legend", true)
+          .style("fill", "black")
+          .style("opacity", self.colorizer["opacity"](x));
+
+        offset += 18;
+      });
+
+      self.legend_svg
+        .append("g")
+        .classed("hiv-trace-legend", true)
+        .attr("transform", "translate(20," + offset + ")")
+        .append("text")
+        .text("missing");
+      self.legend_svg
+        .append("g")
+        .classed("hiv-trace-legend", true)
+        .attr("transform", "translate(0," + offset + ")")
+        .append("circle")
+        .attr("cx", "8")
+        .attr("cy", "-4")
+        .attr("r", "8")
+        .classed("legend", true)
+        .style("fill", "black")
+        .style("opacity", kGlobals.missing.opacity);
+
+      offset += 18;
+    }
+  };
+
+  function compute_cluster_gradient(cluster, cat_id) {
+    if (cat_id) {
+      var id = self.dom_prefix + "-cluster-gradient-" + self.gradient_id++;
+      var gradient = self.network_svg
+        .selectAll("defs")
+        .append("radialGradient")
+        .attr("id", id);
+      var values = _.map(cluster.children, (node) => {
+        var value = self.attribute_node_value_by_id(node, cat_id);
+        return value === kGlobals.missing.label ? Infinity : value;
+      }).sort((a, b) => 0 + a - (0 + b));
+      var finite = _.filter(values, (d) => d < Infinity);
+      var infinite = values.length - finite.length;
+
+      if (infinite) {
+        gradient
+          .append("stop")
+          .attr("offset", "0%")
+          .attr("stop-color", kGlobals.missing.color);
+        gradient
+          .append("stop")
+          .attr("offset", String((infinite / values.length) * 100) + "%")
+          .attr("stop-color", kGlobals.missing.color);
+      }
+
+      _.each(finite, (value, index) => {
+        gradient
+          .append("stop")
+          .attr(
+            "offset",
+            String(((1 + index + infinite) * 100) / values.length) + "%"
+          )
+          .attr("stop-color", self.colorizer["category"](value));
+      });
+      //gradient.append ("stop").attr ("offset", "100%").attr ("stop-color", self.colorizer['category'] (dom[1]));
+
+      return id;
+    }
+    return null;
+  }
+
+  self.handle_attribute_opacity = function (cat_id) {
+    var set_attr = "None";
+
+    ["opacity"].forEach((lbl) => {
+      d3.select(self.get_ui_element_selector_by_role(lbl))
+        .selectAll("li")
+        .selectAll("a")
+        .attr("style", (d, i) => {
+          if (d[1] === cat_id) {
+            set_attr = d[0];
+            return " font-weight: bold;";
+          }
+          return null;
+        });
+      d3.select(self.get_ui_element_selector_by_role(lbl + "_label")).html(
+        __("network_tab")["opacity"] +
+          ": " +
+          set_attr +
+          ' <span class="caret"></span>'
+      );
+    });
+
+    d3.select(self.get_ui_element_selector_by_role("opacity_invert"))
+      .style("display", set_attr === "None" ? "none" : "inline")
+      .classed("btn-active", false)
+      .classed("btn-default", true);
+
+    self.colorizer["opacity_id"] = cat_id;
+    if (cat_id) {
+      var scale = graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["scale"];
+      self.colorizer["opacity_scale"] = d3.scale
+        .linear()
+        .domain([0, kGlobals.network.ContinuousColorStops - 1])
+        .range([0.25, 1]);
+      self.colorizer["opacity"] = function (v) {
+        if (v === kGlobals.missing.label) {
+          return kGlobals.missing.opacity;
+        }
+        return self.colorizer["opacity_scale"](scale(v));
+      };
+    } else {
+      self.colorizer["opacity"] = null;
+      self.colorizer["opacity_scale"] = null;
+    }
+
+    self.draw_attribute_labels();
+    self.update(true);
+    d3.event.preventDefault();
+  };
+
+  self.handle_attribute_continuous = function (cat_id) {
+    var set_attr = "None";
+
+    render_chord_diagram("aux_svg_holder", null, null);
+    render_binned_table("attribute_table", null, null);
+
+    self.network_svg.selectAll("radialGradient").remove();
+
+    self.clusters.forEach((the_cluster) => {
+      delete the_cluster["binned_attributes"];
+      delete the_cluster["gradient"];
+    });
+
+    [
+      ["attributes", false],
+      ["attributes_cat", true],
+    ].forEach((lbl) => {
+      d3.select(self.get_ui_element_selector_by_role(lbl[0], lbl[1]))
+        .selectAll("li")
+        .selectAll("a")
+        .attr("style", (d, i) => {
+          if (d[1] === cat_id) {
+            set_attr = d[0];
+            return " font-weight: bold;";
+          }
+          return null;
+        });
+      d3.select(
+        self.get_ui_element_selector_by_role(lbl[0] + "_label", lbl[1])
+      ).html("Color: " + set_attr + ' <span class="caret"></span>');
+    });
+
+    d3.select(self.get_ui_element_selector_by_role("attributes_invert"))
+      .style("display", set_attr === "None" ? "none" : "inline")
+      .classed("btn-active", false)
+      .classed("btn-default", true);
+
+    if (cat_id) {
+      // map values to inverted scale
+      const color_stops =
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["color_stops"] ||
+        kGlobals.network.ContinuousColorStops;
+
+      if (graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["color_scale"]) {
+        self.colorizer["category"] = graph_data[
+          kGlobals.network.GraphAttrbuteID
+        ][cat_id]["color_scale"](
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id],
+          self
+        );
+
+        self.uniqValues[cat_id]["min"] =
+          self.colorizer["category"](color_stops);
+        self.uniqValues[cat_id]["max"] =
+          self.colorizer["category"](color_stops);
+      } else {
+        self.colorizer["category"] = _.wrap(
+          d3.scale
+            .linear()
+            .domain(_.range(kGlobals.network.ContinuousColorStops))
+            .range(["#fff7ec", "#7f0000"])
+            .interpolate(d3.interpolateRgb),
+          (func, arg) => {
+            self.uniqValues[cat_id]["min"] = "#fff7ec";
+            self.uniqValues[cat_id]["max"] = "#7f0000";
+
+            return func(
+              graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["scale"](
+                arg
+              ) *
+                (1 / kGlobals.network.ContinuousColorStops)
+            );
+          }
+        );
+      }
+
+      if (
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"]
+      ) {
+        // get min and max
+        const min =
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"][
+            "min"
+          ] || self.uniqValues[cat_id]["min"];
+        const max =
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"][
+            "max"
+          ] || self.uniqValues[cat_id]["max"];
+
+        self.uniqValues[cat_id]["min"] =
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"][
+            "min"
+          ] || self.uniqValues[cat_id]["min"];
+        self.uniqValues[cat_id]["max"] =
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"][
+            "max"
+          ] || self.uniqValues[cat_id]["max"];
+
+        self.colorizer["category"] = _.wrap(
+          d3.scale
+            .linear()
+            .domain(_.range(color_stops))
+            .range([min, max])
+            .interpolate(d3.interpolateRgb),
+          (func, arg) =>
+            func(
+              graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["scale"](
+                arg
+              ) *
+                (1 / color_stops)
+            )
+        );
+      }
+
+      self.colorizer["category_id"] = cat_id;
+      self.colorizer["continuous"] = true;
+      self.clusters.forEach((the_cluster) => {
+        the_cluster["gradient"] = compute_cluster_gradient(the_cluster, cat_id);
+      });
+
+      var points = [];
+
+      _.each(self.edges, (e) => {
+        var src = self.attribute_node_value_by_id(
+            self.nodes[e.source],
+            cat_id,
+            true
+          ),
+          tgt = self.attribute_node_value_by_id(
+            self.nodes[e.target],
+            cat_id,
+            true
+          );
+
+        if (src !== kGlobals.missing.label && tgt !== kGlobals.missing.label) {
+          points.push({
+            x: src,
+            y: tgt,
+            title:
+              self.nodes[e.source].id +
+              " (" +
+              src +
+              ") -- " +
+              self.nodes[e.target].id +
+              " (" +
+              tgt +
+              ")",
+          });
+        }
+      });
+      d3.select(
+        self.get_ui_element_selector_by_role("aux_svg_holder_enclosed", true)
+      ).style("display", null);
+
+      scatterPlot.scatterPlot(
+        points,
+        400,
+        400,
+        self.get_ui_element_selector_by_role("aux_svg_holder", true),
+        {
+          x: "Source",
+          y: "Target",
+        },
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["type"] === "Date"
+      );
+    } else {
+      self.colorizer["category"] = null;
+      self.colorizer["category_id"] = null;
+      self.colorizer["continuous"] = false;
+      self.colorizer["category_pairwise"] = null;
+      self.colorizer["category_map"] = null;
+    }
+
+    // Draw color picker for manual override
+    self.renderColorPicker(cat_id, "continuous");
+
+    self.draw_attribute_labels();
+    self.update(true);
+
+    if (d3.event) {
+      d3.event.preventDefault();
+    }
+  };
+
+  self.handle_attribute_categorical = function (cat_id, skip_update) {
+    var set_attr = "None";
+
+    d3.select(self.get_ui_element_selector_by_role("attributes_invert")).style(
+      "display",
+      "none"
+    );
+
+    self.network_svg.selectAll("radialGradient").remove();
+
+    [
+      ["attributes", false],
+      ["attributes_cat", true],
+    ].forEach((lbl) => {
+      d3.select(self.get_ui_element_selector_by_role(lbl[0], lbl[1]))
+        .selectAll("li")
+        .selectAll("a")
+        .attr("style", (d, i) => {
+          if (d[1] === cat_id) {
+            set_attr = d[0];
+            return " font-weight: bold;";
+          }
+          return null;
+        });
+      d3.select(
+        self.get_ui_element_selector_by_role(lbl[0] + "_label", lbl[1])
+      ).html("Color: " + set_attr + ' <span class="caret"></span>');
+    });
+
+    self.clusters.forEach((the_cluster) => {
+      delete the_cluster["gradient"];
+      the_cluster["binned_attributes"] = stratify(
+        attribute_cluster_distribution(the_cluster, cat_id)
+      );
+    });
+
+    self.colorizer["continuous"] = false;
+
+    //TODO -- if preset color scheme does not exist, create one and always use the logic here.
+
+    if (cat_id) {
+      if (cat_id in self.networkColorScheme) {
+        let cat_data =
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["enum"];
+        if (cat_data) {
+          cat_data = new Set(_.map(cat_data, (d) => d.toLowerCase()));
+        }
+        var domain = [],
+          range = [];
+        _.each(self.networkColorScheme[cat_id], (value, key) => {
+          if (cat_data) {
+            if (!cat_data.has(key.toLowerCase())) {
+              return;
+            }
+          }
+          domain.push(key);
+          range.push(value);
+        });
+        self.colorizer["category"] = d3.scale
+          .ordinal()
+          .domain(domain)
+          .range(range);
+      } else if (
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["color_scale"]
+      ) {
+        self.colorizer["category"] = graph_data[
+          kGlobals.network.GraphAttrbuteID
+        ][cat_id]["color_scale"](
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id],
+          self
+        );
+      } else {
+        self.colorizer["category"] = d3.scale
+          .ordinal()
+          .range(kGlobals.Categorical);
+
+        var extended_range = _.clone(self.colorizer["category"].range());
+        extended_range.push(kGlobals.missing.color);
+
+        self.colorizer["category"].domain(
+          _.range(kGlobals.MaximumValuesInCategories + 1)
+        );
+
+        self.colorizer["category"].range(extended_range);
+
+        if (
+          graph_data[kGlobals.network.GraphAttrbuteID][cat_id][
+            "stable-ish order"
+          ]
+        ) {
+          self.colorizer["category"] = _.wrap(
+            self.colorizer["category"],
+            (func, arg) => {
+              if (arg === kGlobals.missing.label) {
+                return func(kGlobals.MaximumValuesInCategories);
+              }
+
+              const ci = graph_data[kGlobals.network.GraphAttrbuteID][cat_id];
+
+              if (ci["reduced_value_range"]) {
+                if (!(arg in ci["reduced_value_range"])) {
+                  arg = kGlobals.network.ReducedValue;
+                }
+              }
+
+              return func(ci["stable-ish order"][arg]);
+            }
+          );
+          //console.log (graph_data[kGlobals.network.GraphAttrbuteID][cat_id]['stable-ish order']);
+        }
+      }
+
+      if (
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["user-defined"]
+      ) {
+        self.colorizer["category"] = _.wrap(
+          self.colorizer["category"],
+          (func, arg) => {
+            if (
+              arg in
+              graph_data[kGlobals.network.GraphAttrbuteID][cat_id][
+                "user-defined"
+              ]
+            ) {
+              return graph_data[kGlobals.network.GraphAttrbuteID][cat_id][
+                "user-defined"
+              ][arg];
+            }
+            return func(arg);
+          }
+        );
+      }
+
+      self.colorizer["category_id"] = cat_id;
+      self.colorizer["category_map"] =
+        graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["value_map"];
+
+      //console.log (cat_id, self.json[kGlobals.network.GraphAttrbuteID][cat_id], graph_data[kGlobals.network.GraphAttrbuteID][cat_id]["value_map"] (null, "lookup"));
+      //self.colorizer['category_map'][null] =  graph_data [kGlobals.network.GraphAttrbuteID][cat_id]['range'];
+
+      //try {
+      //console.log (self.colorizer["category_map"]);
+      self.colorizer["category_pairwise"] = attribute_pairwise_distribution(
+        cat_id,
+        self._aux_get_attribute_dimension(cat_id),
+        self.colorizer["category_map"]
+      );
+      //} catch (err) {
+      // TODO: there are still lingering issues with this "category_map"
+      //}
+
+      render_chord_diagram(
+        "aux_svg_holder",
+        self.colorizer["category_map"],
+        self.colorizer["category_pairwise"]
+      );
+      render_binned_table(
+        "attribute_table",
+        self.colorizer["category_map"],
+        self.colorizer["category_pairwise"]
+      );
+    } else {
+      self.colorizer["category"] = null;
+      self.colorizer["category_id"] = null;
+      self.colorizer["category_pairwise"] = null;
+      self.colorizer["category_map"] = null;
+      render_chord_diagram("aux_svg_holder", null, null);
+      render_binned_table("attribute_table", null, null);
+    }
+    if (self.handle_inline_charts) {
+      self.handle_inline_charts();
+    }
+
+    self.draw_attribute_labels();
+    self.update(true);
+    if (d3.event) {
+      d3.event.preventDefault();
+    }
+
+    // Draw color picker for manual override
+    self.renderColorPicker(cat_id, "categorical");
+  };
+
+  self.filter_visibility = function () {
+    self.clusters.forEach((c) => {
+      c.is_hidden = self.hide_unselected && !c.match_filter;
+    });
+    self.nodes.forEach((n) => {
+      n.is_hidden = self.hide_unselected && !n.match_filter;
+    });
+  };
+
+  self.filter = function (conditions, skip_update) {
+    var anything_changed = false;
+
+    conditions = _.map(["re", "distance", "date"], (cnd) =>
+      _.map(
+        _.filter(conditions, (v) => v.type === cnd),
+        (v) => (cnd === "distance" ? v : v.value)
+      )
+    );
+
+    if (conditions[1].length) {
+      self.nodes.forEach((n) => {
+        n.length_filter = false;
+      });
+
+      _.each(self.edges, (e) => {
+        var did_match = _.some(conditions[1], (d) =>
+          d.greater_than ? e.length >= d.value : e.length < d.value
+        );
+
+        if (did_match) {
+          self.nodes[e.source].length_filter = true;
+          self.nodes[e.target].length_filter = true;
+        }
+        e.length_filter = did_match;
+      });
+    } else {
+      self.nodes.forEach((n) => {
+        n.length_filter = false;
+      });
+      self.edges.forEach((e) => {
+        e.length_filter = false;
+      });
+    }
+
+    if (conditions[2].length) {
+      self.nodes.forEach((n) => {
+        var node_T = self.attribute_node_value_by_id(
+          n,
+          timeDateUtil.getClusterTimeScale()
+        );
+        n.date_filter = _.some(
+          conditions[2],
+          (d) => node_T >= d[0] && node_T <= d[1]
+        );
+      });
+    } else {
+      self.nodes.forEach((n) => {
+        n.date_filter = false;
+      });
+    }
+
+    self.clusters.forEach((c) => {
+      c.match_filter = 0;
+    });
+
+    self.edges.forEach((e) => {
+      if (e.length_filter) {
+        anything_changed = true;
+      }
+    });
+
+    self.nodes.forEach((n) => {
+      var did_match = _.some(
+        conditions[0],
+        (regexp) =>
+          regexp.test(n.id) ||
+          _.some(n[kGlobals.network.NodeAttributeID], (attr) =>
+            regexp.test(attr)
+          )
+      );
+
+      did_match = did_match || n.length_filter || n.date_filter;
+
+      if (did_match !== n.match_filter) {
+        n.match_filter = did_match;
+        anything_changed = true;
+      }
+
+      if (n.match_filter) {
+        n.parent.match_filter += 1;
+      }
+    });
+
+    if (anything_changed && self.handle_inline_charts) {
+      self.handle_inline_charts((n) => n.match_filter);
+    }
+
+    if (anything_changed && !skip_update) {
+      if (self.hide_unselected) {
+        self.filter_visibility();
+      }
+
+      self.update(true);
+    }
+  };
+
+  self.is_empty = function () {
+    return self.cluster_sizes.length === 0;
+  };
+
+  self.link_generator_function = function (d) {
+    var pull = d.pull || 0.0;
+    var path;
+
+    if (pull !== 0.0) {
+      var dist_x = d.target.x - d.source.x;
+      var dist_y = d.target.y - d.source.y;
+      pull *= Math.sqrt(dist_x * dist_x + dist_y * dist_y);
+
+      var theta = Math.PI / 6; // 18deg additive angle
+
+      var alpha = dist_x ? Math.atan(-dist_y / dist_x) : Math.PI / 2; // angle with the X axis
+
+      if (pull < 0) {
+        theta = -theta;
+        pull = -pull;
+      }
+
+      var dx = Math.cos(theta + alpha) * pull,
+        dx2 = Math.cos(theta - alpha) * pull;
+
+      var dy = Math.sin(theta + alpha) * pull,
+        dy2 = Math.sin(theta - alpha) * pull;
+
+      var s1, s2;
+      if (d.target.x >= d.source.x) {
+        s1 = [dx, -dy];
+        s2 = [-dx2, -dy2];
+      } else {
+        s1 = [-dx2, -dy2];
+        s2 = [dx, -dy];
+      }
+
+      path =
+        "M" +
+        d.source.x +
+        " " +
+        d.source.y +
+        " C " +
+        (d.source.x + s1[0]) +
+        " " +
+        (d.source.y + s1[1]) +
+        ", " +
+        (d.target.x + s2[0]) +
+        " " +
+        (d.target.y + s2[1]) +
+        ", " +
+        d.target.x +
+        " " +
+        d.target.y;
+    } else {
+      path =
+        "M" +
+        d.source.x +
+        " " +
+        d.source.y +
+        " L " +
+        d.target.x +
+        " " +
+        d.target.y;
+    }
+
+    d3.select(this).attr("d", path);
+  };
+
+  self.update = function (soft, friction) {
+    self.needs_an_update = false;
+
+    if (options && options["extra-graphics"]) {
+      options["extra-graphics"].call(null, self, options);
+    }
+
+    if (friction) {
+      network_layout.friction(friction);
+    }
+    self.display_warning(self.warning_string, true);
+
+    var rendered_nodes, rendered_clusters, link;
+
+    if (!soft) {
+      var draw_me = prepare_data_to_graph();
+
+      network_layout.nodes(draw_me.all).links(draw_me.edges);
+      update_network_string(draw_me.nodes.length, draw_me.edges.length);
+
+      var edge_set = {};
+
+      _.each(draw_me.edges, (d) => {
+        d.pull = 0.0;
+        var tag;
+
+        if (d.source < d.target) {
+          tag = String(d.source) + "|" + d.target;
+        } else {
+          tag = String(d.target) + "|" + d.source;
+        }
+        if (tag in edge_set) {
+          edge_set[tag].push(d);
+        } else {
+          edge_set[tag] = [d];
+        }
+      });
+
+      _.each(edge_set, (v) => {
+        if (v.length > 1) {
+          var step = 1 / (v.length - 1);
+          _.each(v, (edge, index) => {
+            edge.pull = -0.5 + index * step;
+          });
+        }
+      });
+
+      link = self.network_svg
+        .selectAll(".link")
+        .data(draw_me.edges, (d) => d.id);
+
+      //link.enter().append("line").classed("link", true);
+      link.enter().append("path").classed("link", true);
+      link.exit().remove();
+
+      link
+        .classed("removed", (d) => self.highlight_unsuppored_edges && d.removed)
+        .classed(
+          "unsupported",
+          (d) =>
+            self.highlight_unsuppored_edges &&
+            "support" in d &&
+            d["support"] > 0.05
+        )
+        .classed(
+          "core-link",
+          (d) =>
+            //console.log (d["length"] <= self.core_link_length);
+            d["length"] <= self.core_link_length
+          //return false;
+        );
+
+      link
+        .on("mouseover", edge_pop_on)
+        .on("mouseout", edge_pop_off)
+        .filter((d) => d.directed)
+        .attr("marker-end", "url(#" + self.dom_prefix + "_arrowhead)");
+
+      rendered_nodes = self.network_svg
+        .selectAll(".node")
+        .data(draw_me.nodes, (d) => d.id);
+
+      rendered_nodes.exit().remove();
+
+      /*rendered_nodes.enter().each (function (d) {
+        this.append ("path");
+      });*/
+
+      rendered_nodes.enter().append("g").append("path");
+
+      rendered_clusters = self.network_svg.selectAll(".cluster-group").data(
+        draw_me.clusters.map((d) => d),
+        (d) => d.cluster_id
+      );
+
+      rendered_clusters.exit().remove();
+      rendered_clusters
+        .enter()
+        .append("g")
+        .attr("class", "cluster-group")
+        .attr("transform", (d) => "translate(" + d.x + "," + d.y + ")")
+        .on("click", (d) => network.handle_cluster_click(self, d))
+        .on("mouseover", cluster_pop_on)
+        .on("mouseout", cluster_pop_off)
+        .call(network_layout.drag().on("dragstart", cluster_pop_off));
+
+      self.draw_cluster_table(
+        self.extra_cluster_table_columns,
+        self.cluster_table
+      );
+
+      if (
+        self._is_CDC_ &&
+        !(
+          options &&
+          options["no-subclusters"] &&
+          options["no-subcluster-compute"]
+        )
+      ) {
+        // compute priority clusters
+        self.annotate_priority_clusters(
+          timeDateUtil._networkCDCDateField,
+          36,
+          12
+        );
+
+        try {
+          if (self.isPrimaryGraph) {
+            self.priority_groups_compute_node_membership();
+          }
+        } catch (err) {
+          console.log(err);
+        }
+      }
+
+      if (
+        self._is_CDC_ &&
+        !(options && options["no-subclusters"]) &&
+        options &&
+        options["no-subcluster-compute"]
+      ) {
+        // use precomputed subclusters
+
+        _.each(self.clusters, (cluster_nodes, cluster_index) => {
+          /** extract subclusters; all nodes at given threshold */
+          /** Sub-Cluster: all nodes connected at 0.005 subs/site; there can be multiple sub-clusters per cluster */
+          let subclusters = _.groupBy(
+            cluster_nodes.children,
+            (n) => n.subcluster_id
+          );
+          subclusters = _.values(
+            _.reject(subclusters, (v, k) => k === "undefined")
+          );
+
+          /** sort subclusters by oldest node */
+          _.each(subclusters, (c, i) => {
+            c.sort(oldest_nodes_first);
+          });
+
+          subclusters.sort((c1, c2) => oldest_nodes_first(c1[0], c2[0]));
+
+          subclusters = _.map(subclusters, (c, i) => {
+            const parent_cluster_id = c[0].parent_cluster_id;
+            const subcluster_id = c[0].subcluster_id;
+            const label = c[0].subcluster_label;
+
+            var edges = [];
+
+            // unused var
+            // var meta_data = _.filter(
+            //   hivtrace_cluster_depthwise_traversal(
+            //     cluster_nodes.Nodes,
+            //     cluster_nodes.Edges,
+            //     null,
+            //     edges
+            //   ),
+            //   (cc) => {
+            //     return cc.length > 1;
+            //   }
+            // );
+
+            edges = _.filter(edges, (es) => es.length > 1);
+
+            var stats =
+              self.json.subcluster_summary_stats[parent_cluster_id][
+                subcluster_id
+              ];
+
+            return {
+              children: _.clone(c),
+              parent_cluster: cluster_nodes,
+              cluster_id: label,
+              subcluster_label: subcluster_id,
+              recent_nodes: stats.recent_nodes,
+              priority_score: stats.priority_score,
+              distances: helpers.describe_vector(
+                _.map(edges[i], (e) => e.length)
+              ),
+            };
+          });
+
+          _.each(subclusters, (c) => {
+            _compute_cluster_degrees(c);
+          });
+
+          cluster_nodes.subclusters = subclusters || [];
+
+          // add additional information
+          const stats =
+            self.json.subcluster_summary_stats[cluster_nodes.cluster_id];
+          cluster_nodes.recent_nodes = _.map(
+            _.values(stats),
+            (d) => d.recent_nodes[0] || 0
+          );
+          cluster_nodes.priority_score = _.map(
+            _.values(stats),
+            (d) => d.priority_score[0] || 0
+          );
+        });
+      }
+
+      if (self.subcluster_table) {
+        /*
+            SLKP 20200727 scan subclusters and identify which, if any
+            will need to be automatically created as priority sets
+        */
+
+        // draw subcluster tables
+
+        self.draw_cluster_table(
+          self.extra_subcluster_table_columns,
+          self.subcluster_table,
+          {
+            "no-clusters": true,
+            subclusters: true,
+            headers: function (headers) {
+              headers[0][0].value = "Subcluster ID";
+              headers[0][0].help = "Unique subcluster ID";
+              headers[0][2].help = "Number of total cases in the subcluster";
+            },
+          }
+        );
+      }
+      if (self._is_CDC_) {
+        self.draw_extended_node_table();
+      } else {
+        self.draw_node_table(self.extra_node_table_columns);
+      }
+    } else {
+      rendered_nodes = self.network_svg.selectAll(".node");
+      rendered_clusters = self.network_svg.selectAll(".cluster-group");
+      link = self.network_svg.selectAll(".link");
+      update_network_string(rendered_nodes.size(), link.size());
+    }
+
+    rendered_nodes.each(function (d) {
+      draw_a_node(this, d);
+    });
+
+    rendered_clusters.each(function (d) {
+      draw_a_cluster(this, d);
+    });
+
+    link.style("opacity", (d) =>
+      Math.max(node_opacity(d.target), node_opacity(d.source))
+    );
+
+    if (self.additional_edge_styler) {
+      link.each(function (d) {
+        self.additional_edge_styler(this, d, self);
+      });
+    }
+
+    link
+      .style("display", (d) => {
+        if (d.target.is_hidden || d.source.is_hidden || d.is_hidden) {
+          return "none";
+        }
+        return null;
+      })
+      .classed(
+        "selected_object",
+        (d) => d.ref.length_filter && !self.hide_unselected
+      );
+
+    if (!soft) {
+      currently_displayed_objects =
+        rendered_clusters[0].length + rendered_nodes[0].length;
+
+      network_layout.on("tick", () => {
+        var sizes = network_layout.size();
+
+        rendered_nodes.attr("transform", (d) => {
+          // Defalut values (just to keep nodes in the svg container rectangle).
+          var xBoundLower = 10;
+          var xBoundUpper = sizes[0] - 10;
+          var yBoundLower = 10;
+          var yBoundUpper = sizes[1] - 10;
+
+          if (self.showing_on_map) {
+            const allowed_offset_from_center_of_country = 15;
+            // If the country is in the list that we have, override the default values for the bounds.
+            var country_code = self._get_node_country(d);
+
+            if (country_code in self.countryCentersObject) {
+              const center = self.countryCentersObject[country_code].countryXY;
+
+              xBoundLower = center[0] - allowed_offset_from_center_of_country;
+              xBoundUpper = center[0] + allowed_offset_from_center_of_country;
+              yBoundLower = center[1] - allowed_offset_from_center_of_country;
+              yBoundUpper = center[1] + allowed_offset_from_center_of_country;
+            }
+          }
+
+          return (
+            "translate(" +
+            (d.x = Math.max(xBoundLower, Math.min(xBoundUpper, d.x))) +
+            "," +
+            (d.y = Math.max(yBoundLower, Math.min(yBoundUpper, d.y))) +
+            ")"
+          );
+        });
+        rendered_clusters.attr(
+          "transform",
+          (d) =>
+            "translate(" +
+            (d.x = Math.max(
+              d.rendered_size,
+              Math.min(sizes[0] - d.rendered_size, d.x)
+            )) +
+            "," +
+            (d.y = Math.max(
+              d.rendered_size,
+              Math.min(sizes[1] - d.rendered_size, d.y)
+            )) +
+            ")"
+        );
+
+        link.each(self.link_generator_function);
+      });
+
+      network_layout.start();
+    } else {
+      link.each(self.link_generator_function);
+    }
+  };
+
+  function tick() {
+    var sizes = network_layout.size();
+
+    node
+      .attr("cx", (d) => (d.x = Math.max(10, Math.min(sizes[0] - 10, d.x))))
+      .attr("cy", (d) => (d.y = Math.max(10, Math.min(sizes[1] - 10, d.y))));
+
+    link
+      .attr("x1", (d) => d.source.x)
+      .attr("y1", (d) => d.source.y)
+      .attr("x2", (d) => d.target.x)
+      .attr("y2", (d) => d.target.y);
+  }
+
+  /*------------ Node Methods ---------------*/
+  function compute_node_degrees(nodes, edges) {
+    for (var n in nodes) {
+      nodes[n].degree = 0;
+    }
+
+    for (var e in edges) {
+      nodes[edges[e].source].degree++;
+      nodes[edges[e].target].degree++;
+    }
+  }
+
+  self.attribute_node_value_by_id = function (d, id, number) {
+    try {
+      if (kGlobals.network.NodeAttributeID in d && id) {
+        if (id in d[kGlobals.network.NodeAttributeID]) {
+          let v;
+
+          if (self.json[kGlobals.network.GraphAttrbuteID][id].volatile) {
+            v = self.json[kGlobals.network.GraphAttrbuteID][id].map(d, self);
+          } else {
+            v = d[kGlobals.network.NodeAttributeID][id];
+          }
+
+          if (_.isString(v)) {
+            if (v.length === 0) {
+              return kGlobals.missing.label;
+            } else if (number) {
+              v = Number(v);
+              return _.isNaN(v) ? kGlobals.missing.label : v;
+            }
+          }
+          return v;
+        }
+      }
+    } catch (e) {
+      console.log("self.attribute_node_value_by_id", e, d, id, number);
+    }
+    return kGlobals.missing.label;
+  };
+
+  self.has_network_attribute = function (key) {
+    if (kGlobals.network.GraphAttrbuteID in self.json) {
+      return key in self.json[kGlobals.network.GraphAttrbuteID];
+    }
+    return false;
+  };
+
+  function node_size(d) {
+    if (self.showing_on_map) {
+      return 50;
+    }
+    var r = 5 + Math.sqrt(d.degree); //return (d.match_filter ? 10 : 4)*r*r;
+    return 4 * r * r;
+  }
+
+  function node_color(d) {
+    /*if (d.match_filter) {
+        return "white";
+    }*/
+
+    if (self.colorizer["category_id"]) {
+      var v = self.attribute_node_value_by_id(d, self.colorizer["category_id"]);
+      if (self.colorizer["continuous"]) {
+        if (v === kGlobals.missing.label) {
+          return kGlobals.missing.color;
+        }
+        //console.log (v, self.colorizer['category'](v));
+      }
+      return self.colorizer["category"](v);
+    }
+
+    if (d.hxb2_linked) {
+      return "black";
+    }
+
+    if (d.is_lanl) {
+      return "red";
+    }
+
+    return "gray";
+  }
+
+  function node_opacity(d) {
+    if (self.colorizer["opacity"]) {
+      return self.colorizer["opacity"](
+        self.attribute_node_value_by_id(d, self.colorizer["opacity_id"], true)
+      );
+    }
+    return 1;
+  }
+
+  function cluster_color(d, type) {
+    if (d["binned_attributes"]) {
+      return self.colorizer["category"](type);
+    }
+    return "#bdbdbd";
+  }
+
+  function node_info_string(n) {
+    var str;
+
+    if (!self._is_CDC_) {
+      str =
+        "Degree <em>" +
+        n.degree +
+        "</em><br>Clustering coefficient <em> " +
+        misc.format_value(n.lcc, kGlobals.formats.FloatFormat) +
+        "</em>";
+    } else {
+      str = "# links <em>" + n.degree + "</em>";
+    }
+
+    _.each(
+      _.union(self._additional_node_pop_fields, [
+        self.colorizer["category_id"],
+        self.node_shaper["id"],
+        self.colorizer["opacity_id"],
+      ]),
+      (key) => {
+        if (key) {
+          if (key in graph_data[kGlobals.network.GraphAttrbuteID]) {
+            var attribute = self.attribute_node_value_by_id(n, key);
+
+            if (
+              graph_data[kGlobals.network.GraphAttrbuteID][key]["type"] ===
+              "Date"
+            ) {
+              try {
+                attribute = timeDateUtil.DateViewFormat(attribute);
+              } catch (err) {
+                // do nothing
+              }
+            }
+            if (attribute) {
+              str +=
+                "<br>" +
+                graph_data[kGlobals.network.GraphAttrbuteID][key].label +
+                " <em>" +
+                attribute +
+                "</em>";
+            }
+          }
+        }
+      }
+    );
+
+    return str;
+  }
+
+  function edge_info_string(n) {
+    var str = "Length <em>" + kGlobals.formats.FloatFormat(n.length) + "</em>";
+    if ("support" in n) {
+      str +=
+        "<br>Worst triangle-based support (p): <em>" +
+        kGlobals.formats.FloatFormat(n.support) +
+        "</em>";
+    }
+
+    return str;
+  }
+
+  function node_pop_on(d) {
+    if (d3.event.defaultPrevented) return;
+
+    toggle_tooltip(
+      this,
+      true,
+      (self._is_CDC_ ? "Individual " : "Node ") + d.id,
+      node_info_string(d),
+      self.container
+    );
+  }
+
+  function node_pop_off(d) {
+    if (d3.event.defaultPrevented) return;
+
+    toggle_tooltip(this, false);
+  }
+
+  function edge_pop_on(e) {
+    toggle_tooltip(
+      this,
+      true,
+      e.source.id + " - " + e.target.id,
+      edge_info_string(e),
+      self.container
+    );
+  }
+
+  function edge_pop_off(d) {
+    toggle_tooltip(this, false);
+  }
+
+  /*------------ Cluster Methods ---------------*/
+
+  /* Creates a new object that groups nodes by cluster
+   * @param nodes
+   * @returns clusters
+   */
+  function get_all_clusters(nodes) {
+    var by_cluster = _.groupBy(nodes, "cluster");
+    return by_cluster;
+  }
+
+  function compute_cluster_centroids(clusters) {
+    for (var c in clusters) {
+      var cls = clusters[c];
+      cls.x = 0;
+      cls.y = 0;
+      if (_.has(cls, "children")) {
+        cls.children.forEach((x) => {
+          cls.x += x.x;
+          cls.y += x.y;
+        });
+        cls.x /= cls.children.length;
+        cls.y /= cls.children.length;
+      }
+    }
+  }
+
+  function collapse_cluster(x, keep_in_q) {
+    self.needs_an_update = true;
+    x.collapsed = true;
+    currently_displayed_objects -= self.cluster_sizes[x.cluster_id - 1] - 1;
+    if (!keep_in_q) {
+      var idx = open_cluster_queue.indexOf(x.cluster_id);
+      if (idx >= 0) {
+        open_cluster_queue.splice(idx, 1);
+      }
+    }
+    compute_cluster_centroids([x]);
+    return x.children.length;
+  }
+
+  function expand_cluster(x, copy_coord) {
+    self.needs_an_update = true;
+    x.collapsed = false;
+    currently_displayed_objects += self.cluster_sizes[x.cluster_id - 1] - 1;
+    open_cluster_queue.push(x.cluster_id);
+
+    if (copy_coord) {
+      x.children.forEach((n) => {
+        n.x = x.x + (Math.random() - 0.5) * x.children.length;
+        n.y = x.y + (Math.random() - 0.5) * x.children.length;
+      });
+    } else {
+      x.children.forEach((n) => {
+        n.x = self.width * 0.25 + (Math.random() - 0.5) * x.children.length;
+        n.y = 0.25 * self.height + (Math.random() - 0.5) * x.children.length;
+      });
+    }
+  }
+
+  function render_binned_table(id, the_map, matrix) {
+    var the_table = d3.select(self.get_ui_element_selector_by_role(id, true));
+    if (the_table.empty()) {
+      return;
+    }
+
+    the_table.selectAll("thead").remove();
+    the_table.selectAll("tbody").remove();
+
+    d3.select(
+      self.get_ui_element_selector_by_role(id + "_enclosed", true)
+    ).style("display", matrix ? null : "none");
+
+    if (matrix) {
+      var fill = self.colorizer["category"];
+      var lookup = the_map(null, "lookup");
+
+      var headers = the_table
+        .append("thead")
+        .append("tr")
+        .selectAll("th")
+        .data([""].concat(matrix[0].map((d, i) => lookup[i])));
+
+      headers.enter().append("th");
+      headers
+        .html((d) => "<span>&nbsp;" + d + "</span>")
+        .each(function (d, i) {
+          if (i) {
+            d3.select(this)
+              .insert("i", ":first-child")
+              .classed("fa fa-circle", true)
+              .style("color", () => fill(d));
+          }
+        });
+
+      if (self.show_percent_in_pairwise_table) {
+        var sum = _.map(matrix, (row) => _.reduce(row, (p, c) => p + c, 0));
+
+        matrix = _.map(matrix, (row, row_index) =>
+          _.map(row, (c) => c / sum[row_index])
+        );
+      }
+
+      var rows = the_table
+        .append("tbody")
+        .selectAll("tr")
+        .data(matrix.map((d, i) => [lookup[i]].concat(d)));
+
+      rows.enter().append("tr");
+      rows
+        .selectAll("td")
+        .data((d) => d)
+        .enter()
+        .append("td")
+        .html((d, i) => {
+          if (i === 0) {
+            return "<span>&nbsp;" + d + "</span>";
+          } else if (self.show_percent_in_pairwise_table) {
+            return kGlobals.formats.PercentFormat(d);
+          }
+
+          return d;
+        })
+        .each(function (d, i) {
+          if (i === 0) {
+            d3.select(this)
+              .insert("i", ":first-child")
+              .classed("fa fa-circle", true)
+              .style("color", () => fill(d));
+          }
+        });
+    }
+  }
+
+  function render_chord_diagram(id, the_map, matrix) {
+    var container = d3.select(self.get_ui_element_selector_by_role(id, true));
+
+    if (container.empty()) {
+      return;
+    }
+
+    container.selectAll("svg").remove();
+
+    d3.select(
+      self.get_ui_element_selector_by_role(id + "_enclosed", true)
+    ).style("display", matrix ? null : "none");
+
+    if (matrix) {
+      var lookup = the_map(null, "lookup");
+
+      var svg = container.append("svg");
+
+      var chord = d3.layout
+        .chord()
+        .padding(0.05)
+        .sortSubgroups(d3.descending)
+        .matrix(matrix);
+
+      var text_offset = 20,
+        width = 450,
+        height = 450,
+        innerRadius = Math.min(width, height - text_offset) * 0.41,
+        outerRadius = innerRadius * 1.1;
+
+      var fill = self.colorizer["category"],
+        font_size = 12;
+
+      var text_label = svg
+        .append("g")
+        .attr(
+          "transform",
+          "translate(" + width / 2 + "," + (height - text_offset) + ")"
+        )
+        .append("text")
+        .attr("text-anchor", "middle")
+        .attr("font-size", font_size)
+        .text("");
+
+      svg = svg
+        .attr("width", width)
+        .attr("height", height - text_offset)
+        .append("g")
+        .attr(
+          "transform",
+          "translate(" + width / 2 + "," + (height - text_offset) / 2 + ")"
+        );
+
+      // Returns an event handler for fading a given chord group.
+      const fade = function (opacity, t) {
+        return function (g, i) {
+          text_label.text(t ? lookup[i] : "");
+          svg
+            .selectAll(".chord path")
+            .filter((d) => d.source.index !== i && d.target.index !== i)
+            .transition()
+            .style("opacity", opacity);
+        };
+      };
+
+      svg
+        .append("g")
+        .selectAll("path")
+        .data(chord.groups)
+        .enter()
+        .append("path")
+        .style("fill", (d) => fill(lookup[d.index]))
+        .style("stroke", (d) => fill(lookup[d.index]))
+        .attr(
+          "d",
+          d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius)
+        )
+        .on("mouseover", fade(0.1, true))
+        .on("mouseout", fade(1, false));
+
+      svg
+        .append("g")
+        .attr("class", "chord")
+        .selectAll("path")
+        .data(chord.chords)
+        .enter()
+        .append("path")
+        .attr("d", d3.svg.chord().radius(innerRadius))
+        .style("fill", (d) => fill(d.target.index))
+        .style("opacity", 1);
+    }
+  }
+
+  function attribute_pairwise_distribution(id, dim, the_map, only_expanded) {
+    var scan_from = only_expanded ? draw_me.edges : self.edges;
+    var the_matrix = [];
+    for (var i = 0; i < dim; i += 1) {
+      the_matrix.push([]);
+      for (var j = 0; j < dim; j += 1) {
+        the_matrix[i].push(0);
+      }
+    }
+
+    _.each(scan_from, (edge) => {
+      //console.log (self.attribute_node_value_by_id(self.nodes[edge.source], id), self.attribute_node_value_by_id(self.nodes[edge.target], id));
+      the_matrix[
+        the_map(self.attribute_node_value_by_id(self.nodes[edge.source], id))
+      ][
+        the_map(self.attribute_node_value_by_id(self.nodes[edge.target], id))
+      ] += 1;
+    });
+    // check if there are null values
+
+    var haz_null = the_matrix.some((d, i) => {
+      if (i === dim - 1) {
+        return d.some((d2) => d2 > 0);
+      }
+      return d[dim - 1] > 0;
+    });
+    if (!haz_null) {
+      the_matrix.pop();
+      for (let i = 0; i < dim - 1; i += 1) {
+        the_matrix[i].pop();
+      }
+    }
+
+    // symmetrize the matrix
+
+    dim = the_matrix.length;
+
+    for (let i = 0; i < dim; i += 1) {
+      for (let j = i; j < dim; j += 1) {
+        the_matrix[i][j] += the_matrix[j][i];
+        the_matrix[j][i] = the_matrix[i][j];
+      }
+    }
+
+    return the_matrix;
+  }
+
+  self._aux_populate_category_fields = function (d, k) {
+    d["raw_attribute_key"] = k;
+    if (!("label" in d)) {
+      d["label"] = k;
+    }
+    d.discrete = false;
+
+    if ("enum" in d) {
+      d.discrete = true;
+      d["value_range"] = new Set(d["enum"]);
+      d["value_range"].add(kGlobals.missing.label);
+
+      if (
+        _.every(graph_data.Nodes, (nd) =>
+          d["value_range"].has(self.attribute_node_value_by_id(nd, k))
+        )
+      ) {
+        d["value_range"] = _.clone(d["enum"]);
+        if (!(kGlobals.missing.label in d["value_range"])) {
+          d["value_range"].push(kGlobals.missing.label);
+        }
+        d["dimension"] = d["value_range"].length;
+        d["no-sort"] = true;
+        return d;
+      }
+
+      // confirm that the values in the range are actually in the enum
+    }
+
+    if (d["type"] === "String") {
+      d.discrete = true;
+      d["value_range"] = _.keys(
+        _.countBy(graph_data.Nodes, (nd) =>
+          self.attribute_node_value_by_id(nd, k)
+        )
+      );
+      d["dimension"] = d["value_range"].length;
+    }
+    return d;
+  };
+
+  self._aux_get_attribute_dimension = function (cat_id) {
+    if (cat_id in graph_data[kGlobals.network.GraphAttrbuteID]) {
+      const cinfo = graph_data[kGlobals.network.GraphAttrbuteID][cat_id];
+      if ("reduced_value_range" in cinfo) {
+        return _.size(cinfo["reduced_value_range"]);
+      }
+      return cinfo.dimension;
+    }
+    return 0;
+  };
+
+  self._aux_process_category_values = function (d) {
+    var values,
+      reduced_range = null;
+
+    delete d["reduced_value_range"];
+    if (d["no-sort"]) {
+      values = d["value_range"];
+    } else if (d["type"] === "String") {
+      values = d["value_range"].sort();
+
+      if (d.dimension > kGlobals.MaximumValuesInCategories) {
+        const compressed_values = _.chain(self.nodes)
+          .countBy((node) =>
+            self.attribute_node_value_by_id(node, d["raw_attribute_key"])
+          )
+          .pairs()
+          .sortBy((d) => -d[1])
+          .value();
+
+        reduced_range = [];
+        let i = 0;
+        while (
+          reduced_range.length < kGlobals.MaximumValuesInCategories - 1 &&
+          i < compressed_values.length
+        ) {
+          if (compressed_values[i][0] !== kGlobals.missing.label) {
+            reduced_range.push(compressed_values[i][0]);
+          }
+          i++;
+        }
+        reduced_range = reduced_range.sort();
+        reduced_range.push(kGlobals.network.ReducedValue);
+      }
+
+      var string_hash = function (str) {
+        var hash = 5801;
+        for (var ci = 0; ci < str.length; ci++) {
+          var charCode = str.charCodeAt(ci);
+          hash = (hash << (5 + hash)) + charCode;
+        }
+        return hash;
+      };
+
+      const use_these_values = reduced_range || values;
+
+      var hashed = _.map(use_these_values, string_hash);
+      var available_keys = {};
+      var reindexed = {};
+
+      for (var i = 0; i < kGlobals.MaximumValuesInCategories; i++) {
+        available_keys[i] = true;
+      }
+
+      _.each(hashed, (value, index) => {
+        if (value < 0) {
+          value = -value;
+        }
+
+        var first_try = value % kGlobals.MaximumValuesInCategories;
+        if (first_try in available_keys) {
+          reindexed[use_these_values[index]] = first_try;
+          delete available_keys[first_try];
+          return;
+        }
+
+        var second_try =
+          Math.floor(value / kGlobals.MaximumValuesInCategories) %
+          kGlobals.MaximumValuesInCategories;
+        if (second_try in available_keys) {
+          reindexed[use_these_values[index]] = second_try;
+          delete available_keys[second_try];
+          return;
+        }
+
+        var last_resort = parseInt(_.keys(available_keys).sort()[0]);
+        reindexed[use_these_values[index]] = last_resort;
+        delete available_keys[last_resort];
+      });
+
+      d["stable-ish order"] = reindexed;
+    }
+
+    var map = {};
+
+    if (reduced_range) {
+      const rrl = _.object(_.map(_.pairs(reduced_range), (d) => [d[1], d[0]]));
+
+      _.each(values, (d2, i) => {
+        if (d2 in rrl) {
+          map[d2] = rrl[d2];
+        } else {
+          map[d2] = rrl[kGlobals.network.ReducedValue];
+        }
+      });
+
+      d["reduced_value_range"] = rrl;
+      //console.log (rrl, map);
+      d["value_map"] = function (v, key) {
+        if (key) {
+          //console.log (key, map);
+          return key === "lookup" ? _.invert(rrl) : rrl;
+        }
+        return map[v];
+      };
+    } else {
+      _.each(values, (d2, i) => {
+        map[d2] = i;
+      });
+
+      d["value_map"] = function (v, key) {
+        if (key) {
+          //console.log (key, map);
+          return key === "lookup" ? _.invert(map) : map;
+        }
+        return map[v];
+      };
+    }
+
+    return d;
+  };
+
+  function attribute_cluster_distribution(the_cluster, attribute_id) {
+    if (attribute_id && the_cluster) {
+      return the_cluster.children.map((d) =>
+        self.attribute_node_value_by_id(d, attribute_id)
+      );
+    }
+    return null;
+  }
+
+  function cluster_info_string(id) {
+    var the_cluster = self.clusters[self.cluster_mapping[id]],
+      attr_info = the_cluster["binned_attributes"];
+
+    var str;
+
+    if (self._is_CDC_) {
+      str =
+        "<strong>" +
+        self.cluster_sizes[id - 1] +
+        "</strong> individuals." +
+        "<br>Mean links/individual <em> = " +
+        kGlobals.formats.FloatFormat(the_cluster.degrees["mean"]) +
+        "</em>" +
+        "<br>Max links/individual <em> = " +
+        the_cluster.degrees["max"] +
+        "</em>";
+    } else {
+      str =
+        "<strong>" +
+        self.cluster_sizes[id - 1] +
+        "</strong> nodes." +
+        "<br>Mean degree <em>" +
+        kGlobals.formats.FloatFormat(the_cluster.degrees["mean"]) +
+        "</em>" +
+        "<br>Max degree <em>" +
+        the_cluster.degrees["max"] +
+        "</em>" +
+        "<br>Clustering coefficient <em> " +
+        misc.format_value(the_cluster.cc, kGlobals.formats.FloatFormat) +
+        "</em>";
+    }
+
+    if (attr_info) {
+      attr_info.forEach((d) => {
+        str += "<br>" + d[0] + " <em>" + d[1] + "</em>";
+      });
+    }
+
+    return str;
+  }
+
+  function cluster_pop_on(d) {
+    toggle_tooltip(
+      this,
+      true,
+      "Cluster " + d.cluster_id,
+      cluster_info_string(d.cluster_id),
+      self.container
+    );
+  }
+
+  function cluster_pop_off(d) {
+    toggle_tooltip(this, false);
+  }
+
+  self.expand_cluster_handler = function (d, do_update, move_out) {
+    if (d.collapsed) {
+      var new_nodes = self.cluster_sizes[d.cluster_id - 1] - 1;
+
+      if (new_nodes > max_points_to_render) {
+        self.warning_string = "This cluster is too large to be displayed";
+      } else {
+        var leftover =
+          new_nodes + currently_displayed_objects - max_points_to_render;
+        if (leftover > 0) {
+          var k = 0;
+          for (; k < open_cluster_queue.length && leftover > 0; k++) {
+            var cluster =
+              self.clusters[self.cluster_mapping[open_cluster_queue[k]]];
+            leftover -= cluster.children.length - 1;
+            collapse_cluster(cluster, true);
+          }
+          if (k || open_cluster_queue.length) {
+            open_cluster_queue.splice(0, k);
+          }
+        }
+
+        if (leftover <= 0) {
+          expand_cluster(d, !move_out);
+        }
+      }
+
+      if (do_update) {
+        self.update(false, 0.6);
+      }
+    }
+    return "";
+  };
+
+  function show_sequences_in_cluster(d) {
+    var sequences = {};
+    _.each(
+      self.extract_single_cluster(
+        self.clusters[self.cluster_mapping[d.cluster]].children,
+        null,
+        true
+      ).Edges,
+      (e) => {
+        _.each(e.sequences, (s) => {
+          if (!(s in sequences)) {
+            sequences[s] = 1;
+          }
+        });
+      }
+    );
+    //console.log (_.keys(sequences));
+  }
+
+  function _compute_cluster_degrees(d) {
+    var degrees = d.children.map((c) => c.degree);
+    degrees.sort(d3.ascending);
+    d.degrees = helpers.describe_vector(degrees);
+  }
+
+  function handle_node_label(container, node) {
+    node.show_label = !node.show_label;
+    self.update(true);
+  }
+
+  function collapse_cluster_handler(d, do_update) {
+    collapse_cluster(self.clusters[self.cluster_mapping[d.cluster]]);
+    if (do_update) {
+      self.update(false, 0.4);
+    }
+  }
+
+  function cluster_box_size(c) {
+    return 8 * Math.sqrt(c.children.length);
+  }
+
+  self.extract_network_time_series = function (
+    time_attr,
+    other_attributes,
+    node_filter
+  ) {
+    var use_these_nodes = node_filter
+      ? _.filter(self.nodes, node_filter)
+      : self.nodes;
+
+    var result = _.map(use_these_nodes, (node) => {
+      var series = {
+        time: self.attribute_node_value_by_id(node, time_attr),
+      };
+      if (other_attributes) {
+        _.each(other_attributes, (attr, key) => {
+          series[attr] = self.attribute_node_value_by_id(node, key);
+        });
+      }
+      return series;
+    });
+
+    result.sort((a, b) => {
+      if (a.time < b.time) return -1;
+      if (a.time === b.time) return 0;
+      return 1;
+    });
+
+    return result;
+  };
+
+  self.expand_some_clusters = function (subset) {
+    subset = subset || self.clusters;
+    subset.forEach((x) => {
+      if (!x.is_hidden) {
+        self.expand_cluster_handler(x, false);
+      }
+    });
+    self.update();
+  };
+
+  self.select_some_clusters = function (condition) {
+    return self.clusters.filter((c, i) =>
+      _.some(c.children, (n) => condition(n))
+    );
+  };
+
+  self.collapse_some_clusters = function (subset) {
+    subset = subset || self.clusters;
+    subset.forEach((x) => {
+      if (!x.collapsed) collapse_cluster(x);
+    });
+    self.update();
+  };
+
+  self.toggle_hxb2 = function () {
+    self.hide_hxb2 = !self.hide_hxb2;
+    self.update();
+  };
+
+  self.toggle_diff = function () {
+    self.showing_diff = !self.showing_diff;
+    if (self.showing_diff) {
+      self.cluster_filtering_functions["new"] = self.filter_if_added;
+    } else {
+      delete self.cluster_filtering_functions["new"];
+    }
+    self.update();
+  };
+
+  self.toggle_highlight_unsupported_edges = function () {
+    self.highlight_unsuppored_edges = !self.highlight_unsuppored_edges;
+    self.update();
+  };
+
+  self.toggle_time_filter = function () {
+    if (self.using_time_filter) {
+      self.using_time_filter = null;
+    } else {
+      self.using_time_filter = timeDateUtil.getCurrentDate();
+      self.using_time_filter.setFullYear(
+        self.using_time_filter.getFullYear() - 1
+      );
+    }
+
+    if (self.using_time_filter) {
+      self.cluster_filtering_functions["recent"] = self.filter_time_period;
+    } else {
+      delete self.cluster_filtering_functions["recent"];
+    }
+    self.update();
+  };
+
+  function stratify(array) {
+    if (array) {
+      var dict = {},
+        stratified = [];
+
+      array.forEach((d) => {
+        if (d in dict) {
+          dict[d] += 1;
+        } else {
+          dict[d] = 1;
+        }
+      });
+      for (var uv in dict) {
+        stratified.push([uv, dict[uv]]);
+      }
+      return stratified.sort((a, b) => a[0] - b[0]);
+    }
+    return array;
+  }
+
+  self._distance_gate_options = function (threshold) {
+    threshold = threshold || 0.005;
+
+    return {
+      "edge-styler": function (element, d, network) {
+        var e_type = misc.edge_typer(
+          d,
+          network.edge_types,
+          network.edge_cluster_threshold
+        );
+        if (e_type !== "") {
+          d3.select(element).style(
+            "stroke",
+            network._edge_colorizer(
+              misc.edge_typer(
+                d,
+                network.edge_types,
+                network.edge_cluster_threshold
+              )
+            )
+          ); //.style ("stroke-dasharray", network._edge_dasher (d["edge_type"]));
+        }
+        d.is_hidden = !network.shown_types[e_type];
+        d3.select(element).style("stroke-width", "4px");
+      },
+
+      init_code: function (network) {
+        function style_edge(type) {
+          this.style("stroke-width", "5px");
+          if (type.length) {
+            this.style("stroke", network._edge_colorizer(type)); //.style ("stroke-dasharray", network._edge_dasher (type));
+          } else {
+            this.classed("link", true);
+            var def_color = this.style("stroke");
+            this.classed("link", null);
+            this.style("stroke", def_color);
+          }
+        }
+
+        network.update_cluster_threshold_display = (T) => {
+          network.edge_cluster_threshold = T;
+          network.edge_types = [
+            "≤" + network.edge_cluster_threshold,
+            ">" + network.edge_cluster_threshold,
+          ];
+
+          network._edge_colorizer = d3.scale
+            .ordinal()
+            .range(kGlobals.EdgeColorBase)
+            .domain(network.edge_types);
+          //network._edge_dasher   = _edge_dasher;
+          network.shown_types = _.object(
+            _.map(network.edge_types, (d) => [d, 1])
+          );
+          network.edge_legend = {
+            caption: "Links by distance",
+            types: {},
+          };
+
+          _.each(network.shown_types, (ignore, t) => {
+            if (t.length) {
+              network.edge_legend.types[t] = _.partial(style_edge, t);
+            }
+          });
+        };
+
+        network.update_cluster_threshold_display(threshold);
+      },
+
+      extra_menu: {
+        title: "Additional options",
+        items: [
+          [
+            function (network, item) {
+              //console.log(network.edge_cluster_threshold);
+              var enclosure = item.append("div").classed("form-group", true);
+              enclosure
+                .append("label")
+                .text("Genetic distance threshold ")
+                .classed("control-label", true);
+              enclosure
+                .append("input")
+                .classed("form-control", true)
+                .attr("value", String(network.edge_cluster_threshold))
+                .on("change", function (e) {
+                  //d3.event.preventDefault();
+                  if (this.value) {
+                    const newT = parseFloat(this.value);
+                    if (_.isNumber(newT) && newT > 0.0 && newT < 1) {
+                      network.update_cluster_threshold_display(newT);
+                      network.draw_attribute_labels();
+                      network.update(true);
+                      enclosure
+                        .classed("has-success", true)
+                        .classed("has-error", false);
+                      return;
+                    }
+                  }
+
+                  enclosure
+                    .classed("has-success", false)
+                    .classed("has-error", true);
+                })
+                .on("click", (e) => {
+                  d3.event.stopPropagation();
+                });
+            },
+            null,
+          ],
+        ],
+      },
+    };
+  };
+
+  self._social_view_options = function (
+    labeled_links,
+    shown_types,
+    edge_typer
+  ) {
+    edge_typer =
+      edge_typer ||
+      function (e) {
+        return _.has(e, "edge_type") ? e["edge_type"] : "";
+      };
+
+    return {
+      "edge-styler": function (element, d, network) {
+        var e_type = misc.edge_typer(d);
+        if (e_type !== "") {
+          d3.select(element).style(
+            "stroke",
+            network._edge_colorizer(misc.edge_typer(d))
+          ); //.style ("stroke-dasharray", network._edge_dasher (d["edge_type"]));
+
+          d.is_hidden = !network.shown_types[e_type];
+        } else {
+          d.is_hidden = !network.shown_types[""];
+        }
+        d3.select(element).style("stroke-width", "5px");
+      },
+
+      init_code: function (network) {
+        function style_edge(type) {
+          this.style("stroke-width", "5px");
+          if (type.length) {
+            this.style("stroke", network._edge_colorizer(type)); //.style ("stroke-dasharray", network._edge_dasher (type));
+          } else {
+            this.classed("link", true);
+            var def_color = this.style("stroke");
+            this.classed("link", null);
+            this.style("stroke", def_color);
+          }
+        }
+
+        var edge_types = _.keys(shown_types);
+        edge_types.sort();
+
+        network._edge_colorizer = d3.scale
+          .ordinal()
+          .range(kGlobals.CategoricalBase)
+          .domain(edge_types);
+        //network._edge_dasher   = _edge_dasher;
+        network.shown_types = _.clone(shown_types);
+        network.edge_legend = {
+          caption: "Network links",
+          types: {},
+        };
+
+        _.each(network.shown_types, (ignore, t) => {
+          if (t.length) {
+            network.edge_legend.types[t] = _.partial(style_edge, t);
+          } else {
+            network.edge_legend.types["Molecular links"] = _.partial(
+              style_edge,
+              t
+            );
+          }
+        });
+      },
+
+      extra_menu: {
+        title: "Additional options",
+        items: _.map(labeled_links, (edge_class) => [
+          function (network, element) {
+            function toggle_element() {
+              network.shown_types[edge_class] =
+                !network.shown_types[edge_class];
+              checkbox.attr(
+                "checked",
+                network.shown_types[edge_class] ? "" : null
+              );
+              network.update(true);
+            }
+
+            var link;
+
+            if (edge_class.length) {
+              link = element
+                .append("a")
+                .text(edge_class + " links")
+                .style("color", network._edge_colorizer(edge_class))
+                .on("click", toggle_element);
+            } else {
+              link = element
+                .append("a")
+                .text("Molecular links")
+                .on("click", toggle_element);
+            }
+            var checkbox = link
+              .append("input")
+              .attr("type", "checkbox")
+              .attr("checked", "");
+          },
+        ]),
+      },
+    };
+  };
+
+  /*------------ Node injection (social network) ---------------*/
+
+  self.load_nodes_edges = function (
+    nodes_and_attributes,
+    index_id,
+    edges_and_attributes,
+    annotation
+  ) {
+    annotation = annotation || "Social";
+    /**
+        1. Scan the list of nodes for
+            a. Nodes not present in the existing network
+            b. Attribute names
+            c. Attribute values
+
+        2. Scan the list of edges for
+            a. Edges not present in the existing network
+            b. Attribute names
+            c. Attribute values
+     */
+
+    var new_nodes = [];
+    var edge_types_dict = {};
+    var existing_nodes = 0;
+    var injected_nodes = {};
+    var node_attributes = {};
+    var existing_network_nodes = {};
+    var node_name_2_id = {};
+
+    _.each(self.json.Nodes, (n, i) => {
+      existing_network_nodes[n.id] = n;
+      node_name_2_id[n.id] = i;
+    });
+
+    const handle_node_attributes = (target, n) => {
+      _.each(n, (attribute_value, attribute_key) => {
+        if (attribute_key !== index_id) {
+          HTX.HIVTxNetwork.inject_attribute_node_value_by_id(
+            target,
+            attribute_key,
+            attribute_value
+          );
+        }
+      });
+    };
+
+    const inject_new_node = (node_name, n) => {
+      const new_node = {
+        node_class: "injected",
+        node_annotation: annotation,
+        attributes: [],
+        degree: 0,
+      };
+      new_node[kGlobals.network.NodeAttributeID] = {};
+      new_node.id = node_name;
+      handle_node_attributes(new_node, n);
+      node_name_2_id[node_name] = self.json.Nodes.length;
+      self.json.Nodes.push(new_node);
+      new_nodes.push(new_node);
+    };
+
+    if (nodes_and_attributes && nodes_and_attributes.length) {
+      if (!(index_id in nodes_and_attributes[0])) {
+        throw Error(
+          index_id +
+            " is not one of the attributes in the imported node records"
+        );
+      }
+
+      _.each(nodes_and_attributes[0], (r, i) => {
+        if (i !== index_id) {
+          var attribute_definition = {
+            label: i,
+            type: "String",
+            annotation: annotation,
+          };
+          self.inject_attribute_description(i, attribute_definition);
+        }
+      });
+
+      _.each(nodes_and_attributes, (n) => {
+        if (n[index_id] in existing_network_nodes) {
+          handle_node_attributes(existing_network_nodes[n[index_id]], n);
+          existing_nodes++;
+        } else {
+          inject_new_node(n[index_id], n);
+        }
+      });
+    }
+
+    if (edges_and_attributes && edges_and_attributes.length) {
+      const auto_inject = !(
+        nodes_and_attributes && nodes_and_attributes.length
+      );
+
+      if (auto_inject) {
+        _.map(existing_network_nodes, (e) => false);
+      }
+
+      _.each(edges_and_attributes, (e) => {
+        try {
+          if ("Index" in e && "Partner" in e && "Contact" in e) {
+            if (!(e["Index"] in node_name_2_id)) {
+              if (auto_inject) {
+                inject_new_node(e["Index"], []);
+              } else {
+                throw Error("Invalid index node");
+              }
+            } else if (auto_inject) {
+              existing_network_nodes[e["Index"]] = true;
+            }
+
+            if (!(e["Partner"] in node_name_2_id)) {
+              if (auto_inject) {
+                inject_new_node(e["Partner"], []);
+              } else {
+                throw Error("Invalid partner node");
+              }
+            } else if (auto_inject) {
+              existing_network_nodes[e["Partner"]] = true;
+            }
+
+            edge_types_dict[e["Contact"]] =
+              (edge_types_dict[e["Contact"]]
+                ? edge_types_dict[e["Contact"]]
+                : 0) + 1;
+
+            var new_edge = {
+              source: node_name_2_id[e["Index"]],
+              target: node_name_2_id[e["Partner"]],
+              edge_type: e["Contact"],
+              length: 0.005,
+              directed: true,
+            };
+
+            self.json.Edges.push(new_edge);
+          } else {
+            throw Error("Missing required attribute");
+          }
+        } catch (err) {
+          throw Error(
+            "Invalid edge specification ( " + err + ") " + JSON.stringify(e)
+          );
+        }
+      });
+
+      if (auto_inject) {
+        existing_nodes = _.size(_.filter(existing_network_nodes, (e) => e));
+      }
+
+      self._aux_populate_category_menus();
+
+      self.update_clusters_with_injected_nodes(null, null, annotation);
+      if (self._is_CDC_) {
+        self.draw_extended_node_table(self.json.Nodes);
+      } else {
+        self.draw_node_table(self.extra_node_table_columns, self.json.Nodes);
+      }
+      if (!self.extra_cluster_table_columns) {
+        self.extra_cluster_table_columns = [];
+      }
+      if (!self.extra_subcluster_table_columns) {
+        self.extra_subcluster_table_columns = [];
+      }
+
+      var edge_types_by_cluster = {};
+      _.each(self.json.Edges, (e) => {
+        try {
+          var edge_clusters = _.union(
+            _.keys(self.json.Nodes[e.source].extended_cluster),
+            _.keys(self.json.Nodes[e.target].extended_cluster)
+          );
+          _.each(edge_clusters, (c) => {
+            if (!(c in edge_types_by_cluster)) {
+              edge_types_by_cluster[c] = {};
+            }
+            if (e.edge_type) {
+              edge_types_by_cluster[c][e.edge_type] = 1;
+            }
+          });
+        } catch (err) {
+          console.log(err);
+        }
+      });
+
+      var edge_types_by_cluster_sorted = {};
+      _.each(edge_types_by_cluster, (v, c) => {
+        var my_keys = _.keys(v);
+        my_keys.sort();
+        edge_types_by_cluster_sorted[c] = my_keys;
+      });
+
+      /*var _edge_dasher = d3.scale
+        .ordinal()
+        .range(kGlobals.CategoricalDashPatterns)
+        .domain(edge_types);
+      */
+
+      var _social_view_handler = function (
+        id,
+        node_filter,
+        labeled_links,
+        shown_types,
+        title,
+        e
+      ) {
+        self.open_exclusive_tab_view(
+          id,
+          node_filter,
+          title,
+          self._social_view_options(labeled_links, shown_types),
+          true
+        );
+      };
+
+      var _injected_column_subcluster_button_handler = function (
+        payload,
+        edge_filter,
+        title,
+        e
+      ) {
+        function edge_filter_for_subclusters(edge) {
+          return (
+            HTX.HIVTxNetwork.is_edge_injected(edge) ||
+            edge.length <= self.subcluster_threshold
+          );
+        }
+
+        var subcluster_edges = [];
+
+        var direct_links_only = hivtrace_cluster_depthwise_traversal(
+          self.json.Nodes,
+          self.json.Edges,
+          edge_filter || edge_filter_for_subclusters,
+          //null,
+          subcluster_edges,
+          payload.children
+        );
+
+        var labeled_links = {},
+          shown_types = {};
+        _.each(subcluster_edges[0], (e) => {
+          if (e.edge_type) {
+            labeled_links[e.edge_type] = 1;
+            shown_types[e.edge_type] = 1;
+          }
+        });
+
+        labeled_links = _.keys(labeled_links);
+        labeled_links.sort();
+        labeled_links.push("");
+        shown_types[""] = 1;
+
+        title =
+          title ||
+          function (id) {
+            return (
+              "Subcluster " + payload.cluster_id + "[+ " + annotation + "]"
+            );
+          };
+
+        var cv = self.view_subcluster(
+          payload,
+          direct_links_only[0],
+          title(payload.cluster_id),
+          self._social_view_options(labeled_links, shown_types),
+          edge_filter_for_subclusters,
+          true
+        );
+        //cv.annotate_priority_clusters(timeDateUtil._networkCDCDateField, 36, 12);
+        //cv.handle_attribute_categorical("recent_rapid");
+        cv._refresh_subcluster_view(
+          self.today || timeDateUtil.getCurrentDate()
+        );
+      };
+
+      var injected_column_subcluster = [
+        {
+          description: {
+            value: annotation + " network",
+            help: "View subclusters with " + annotation + " data",
+          },
+
+          generator: function (cluster) {
+            return {
+              value: cluster,
+              callback: function (element, payload) {
+                var this_cell = d3.select(element);
+                this_cell
+                  .append("button")
+                  .classed("btn btn-primary btn-xs pull-right", true)
+                  .style("margin-left", "1em")
+                  .text("Complete " + annotation)
+                  .on(
+                    "click",
+                    _.partial(
+                      _injected_column_subcluster_button_handler,
+                      payload,
+                      null,
+                      null
+                    )
+                  );
+
+                var node_ids = {};
+
+                _.each(payload.children, (n) => {
+                  node_ids[n.id] = 1;
+                });
+
+                this_cell
+                  .append("button")
+                  .classed("btn btn-primary btn-xs pull-right", true)
+                  .text("Directly linked " + annotation)
+                  .on(
+                    "click",
+                    _.partial(
+                      _injected_column_subcluster_button_handler,
+                      payload,
+                      (edge) =>
+                        self.json.Nodes[edge.target].id in node_ids ||
+                        self.json.Nodes[edge.source].id in node_ids,
+                      (id) =>
+                        "Subcluster " +
+                        payload.cluster_id +
+                        "[+ direct  " +
+                        annotation +
+                        "]"
+                    )
+                  );
+              },
+            };
+          },
+        },
+      ];
+
+      var injected_column = [
+        {
+          description: {
+            value: annotation + " network",
+            sort: function (c) {
+              return c.value[0];
+            },
+            help: "Nodes added and clusters merged through " + annotation,
+          },
+          generator: function (cluster) {
+            return {
+              value: [
+                cluster.injected[annotation],
+                cluster.linked_clusters,
+                cluster.cluster_id,
+              ],
+
+              callback: function (element, payload) {
+                var this_cell = d3.select(element);
+                this_cell.text(
+                  Number(payload[0]) + " " + annotation + " nodes. "
+                );
+                var other_clusters = [];
+                if (payload[1]) {
+                  other_clusters = _.without(_.keys(payload[1]), payload[2]);
+                  if (other_clusters.length) {
+                    other_clusters.sort();
+                    this_cell
+                      .append("span")
+                      .classed("label label-info", true)
+                      .text("Bridges to " + other_clusters.length + " clusters")
+                      .attr("title", other_clusters.join(", "));
+                  }
+                }
+
+                var labeled_links = _.clone(
+                  edge_types_by_cluster_sorted[payload[2]]
+                );
+
+                if (
+                  payload[0] > 0 ||
+                  other_clusters.length ||
+                  (edge_types_by_cluster_sorted[payload[2]] &&
+                    labeled_links.length)
+                ) {
+                  labeled_links.push("");
+
+                  var shown_types = {};
+                  _.each(labeled_links, (t) => {
+                    shown_types[t] = 1;
+                  });
+
+                  this_cell
+                    .append("button")
+                    .classed("btn btn-primary btn-xs pull-right", true)
+                    .text("Directly linked " + annotation)
+                    .style("margin-left", "1em")
+                    .on("click", (e) => {
+                      var directly_linked_ids = {};
+                      var node_ids = {};
+
+                      _.each(cluster.children, (n) => {
+                        node_ids[n.id] = 1;
+                      });
+
+                      var direct_links_only =
+                        hivtrace_cluster_depthwise_traversal(
+                          self.json.Nodes,
+                          self.json.Edges,
+                          (edge) =>
+                            self.json.Nodes[edge.target].id in node_ids ||
+                            self.json.Nodes[edge.source].id in node_ids,
+                          false,
+                          cluster.children
+                        );
+
+                      _.each(direct_links_only[0], (n) => {
+                        directly_linked_ids[n.id] = true;
+                      });
+
+                      //console.log (directly_linked_ids);
+
+                      _social_view_handler(
+                        payload[2],
+                        (n) => n.id in directly_linked_ids,
+                        labeled_links,
+                        shown_types,
+                        (id) =>
+                          "Cluster " + id + "[+ direct " + annotation + "]",
+                        e
+                      );
+                    });
+
+                  this_cell
+                    .append("button")
+                    .classed("btn btn-primary btn-xs pull-right", true)
+                    .text("Complete " + annotation)
+                    .on(
+                      "click",
+                      _.partial(
+                        _social_view_handler,
+                        payload[2],
+                        (n) =>
+                          n.extended_cluster &&
+                          payload[2] in n.extended_cluster,
+                        labeled_links,
+                        shown_types,
+                        (id) => "Cluster " + id + "[+ " + annotation + "]"
+                      )
+                    );
+                }
+              },
+            };
+          },
+        },
+      ];
+
+      if (self.extra_cluster_table_columns) {
+        self.extra_cluster_table_columns =
+          self.extra_cluster_table_columns.concat(injected_column);
+      } else {
+        self.extra_cluster_table_columns = injected_column;
+      }
+
+      self.draw_cluster_table(
+        self.extra_cluster_table_columns,
+        self.cluster_table
+      );
+
+      if (self.subcluster_table) {
+        if (self.extra_subcluster_table_columns) {
+          self.extra_subcluster_table_columns =
+            self.extra_subcluster_table_columns.concat(
+              injected_column_subcluster
+            );
+        } else {
+          self.extra_subcluster_table_columns = injected_column_subcluster;
+        }
+        self.draw_cluster_table(
+          self.extra_subcluster_table_columns,
+          self.subcluster_table,
+          { subclusters: true, "no-clusters": true }
+        );
+      }
+    }
+
+    return {
+      nodes: new_nodes,
+      existing_nodes: existing_nodes,
+      edges: edge_types_dict,
+    };
+  };
+
+  self.update_clusters_with_injected_nodes = function (
+    node_filter,
+    edge_filter,
+    annotation
+  ) {
+    let recomputed_clusters;
+
+    try {
+      node_filter =
+        node_filter ||
+        function () {
+          return true;
+        };
+      edge_filter =
+        edge_filter ||
+        function () {
+          return true;
+        };
+
+      recomputed_clusters = hivtrace_cluster_depthwise_traversal(
+        _.filter(self.json.Nodes, node_filter),
+        self.json.Edges,
+        null,
+        false
+      );
+
+      _.each(recomputed_clusters, (c) => {
+        var cluster_ids = {};
+        var injected_count = 0;
+
+        _.each(c, (n) => {
+          cluster_ids[n.cluster] = 1;
+          injected_count += n.cluster ? 0 : 1;
+        });
+
+        //var cluster_ids = _.keys (cluster_ids);
+
+        //console.log (cluster_ids.length);
+
+        // count how many "injected" nodes are there in the new cluster
+
+        if (injected_count) {
+          delete cluster_ids[undefined];
+        }
+
+        _.each(c, (n) => {
+          if ("extended_cluster" in n) {
+            _.extend(n["extended_cluster"], cluster_ids);
+          } else {
+            n["extended_cluster"] = cluster_ids;
+          }
+        });
+
+        _.each(cluster_ids, (c, k) => {
+          var existing_cluster = self.clusters[self.cluster_mapping[k]];
+          if (!existing_cluster.injected) {
+            existing_cluster.injected = {};
+          }
+          existing_cluster.injected[annotation] = injected_count;
+          if ("linked_clusters" in existing_cluster) {
+            _.extend(existing_cluster["linked_clusters"], cluster_ids);
+          } else {
+            existing_cluster["linked_clusters"] = cluster_ids;
+          }
+        });
+      });
+    } catch (err) {
+      console.log(err);
+      throw err;
+    }
+
+    return recomputed_clusters;
+  };
+  /*------------ Event Functions ---------------*/
+  function toggle_tooltip(element, turn_on, title, tag, container) {
+    //if (d3.event.defaultPrevented) return;
+    if (!element) {
+      return;
+    }
+
+    if (turn_on && !element.tooltip) {
+      // check to see if there are any other tooltips shown
+      $("[role='tooltip']").each(function (d) {
+        $(this).remove();
+      });
+
+      var this_box = $(element);
+
+      // var this_data = d3.select(element).datum();
+      //this_data.fixed = true;
+
+      element.tooltip = this_box.tooltip({
+        title: title + "<br>" + tag,
+        html: true,
+        container: container ? container : "body",
+      });
+
+      _.delay(_.bind(element.tooltip.tooltip, element.tooltip), 500, "show");
+    } else if (!turn_on && element.tooltip) {
+      element.tooltip.tooltip("destroy");
+      element.tooltip = undefined;
+    }
+  }
+
+  /*------------ Init code ---------------*/
+
+  var l_scale = 5000, // link scale
+    graph_data = self.json, // the raw JSON network object
+    max_points_to_render = 2048,
+    singletons = 0,
+    open_cluster_queue = [],
+    currently_displayed_objects,
+    gravity_scale = d3.scale
+      .pow()
+      .exponent(0.5)
+      .domain([1, 100000])
+      .range([0.1, 0.15]),
+    link_scale = d3.scale.pow().exponent(1.25).clamp(true).domain([0, 0.1]);
+
+  /*------------ D3 globals and SVG elements ---------------*/
+
+  var network_layout = d3.layout
+    .force()
+    .on("tick", tick)
+    .charge((d) => {
+      if (self.showing_on_map) {
+        return -60;
+      }
+      if (d.cluster_id) {
+        return self.charge_correction * (-15 - 5 * d.children.length ** 0.4);
+      }
+      return self.charge_correction * (-10 - 5 * Math.sqrt(d.degree));
+    })
+    .linkDistance(
+      (d) => link_scale(d.length) * l_scale * 0.2 //Math.max(d.length, 0.005) * l_scale * 10;
+    )
+    .linkStrength((d) => {
+      if (self.showing_on_map) {
+        return 0.01;
+      }
+      if (d.support !== undefined) {
+        return 0.75 - 0.5 * d.support;
+      }
+      return 1;
+    })
+    .chargeDistance(l_scale * 0.1)
+    .gravity(self.showing_on_map ? 0 : gravity_scale(json.Nodes.length))
+    .friction(0.25);
+
+  d3.select(self.container).selectAll(".my_progress").style("display", "none");
+  d3.select(self.container).selectAll("svg").remove();
+  nodesTab.getNodeTable().selectAll("*").remove();
+  self.cluster_table.selectAll("*").remove();
+
+  self.network_svg = d3
+    .select(self.container)
+    .append("svg:svg")
+    //.style ("border", "solid black 1px")
+    .attr("id", self.dom_prefix + "-network-svg")
+    .attr("width", self.width + self.margin.left + self.margin.right)
+    .attr("height", self.height + self.margin.top + self.margin.bottom);
+
+  self.network_cluster_dynamics = null;
+
+  //.append("g")
+  // .attr("transform", "translate(" + self.margin.left + "," + self.margin.top + ")");
+
+  var legend_drag = d3.behavior
+    .drag()
+    .on("dragstart", () => {
+      d3.event.sourceEvent.stopPropagation();
+    })
+    .on("drag", function (d) {
+      d3.select(this).attr(
+        "transform",
+        "translate(" + [d3.event.x, d3.event.y] + ")"
+      );
+    });
+  const legend_vertical_offset = self.showing_on_map ? 100 : 5;
+  self.legend_svg = self.network_svg
+    .append("g")
+    .attr("transform", "translate(5," + legend_vertical_offset + ")")
+    .call(legend_drag);
+
+  self.network_svg
+    .append("defs")
+    .append("marker")
+    .attr("id", self.dom_prefix + "_arrowhead")
+    .attr("refX", 18)
+    .attr("refY", 6)
+    .attr("markerWidth", 20)
+    .attr("markerHeight", 16)
+    .attr("orient", "auto")
+    .attr("stroke", "#666666")
+    .attr("markerUnits", "userSpaceOnUse")
+    .attr("fill", "#AAAAAA")
+    .append("path")
+    .attr("d", "M 0,0 L 2,6 L 0,12 L14,6 Z"); //this is actual shape for arrowhead
+
+  change_window_size();
+
+  initial_json_load();
+
+  if (options) {
+    if (_.isNumber(options["charge"])) {
+      self.charge_correction = options["charge"];
+    }
+
+    if ("colorizer" in options) {
+      self.colorizer = options["colorizer"];
+    }
+
+    if ("node_shaper" in options) {
+      self.node_shaper = options["node_shaper"];
+    }
+
+    if ("callbacks" in options) {
+      options["callbacks"](self);
+    }
+
+    if (_.isArray(options["expand"])) {
+      self.expand_some_clusters(
+        _.filter(
+          self.clusters,
+          (c) => options["expand"].indexOf(c.cluster_id) >= 0
+        )
+      );
+    }
+
+    if (options["priority-sets-url"]) {
+      const is_writeable = options["is-writeable"];
+      self.load_priority_sets(options["priority-sets-url"], is_writeable);
+    }
+
+    if (self.showing_diff) {
+      self.handle_attribute_categorical("_newly_added");
+    }
+  }
+
+  self.draw_attribute_labels();
+  d3.select(self.container).selectAll(".my_progress").style("display", "none");
+  network_layout.start();
+
+  return self;
+};
+
+export {
+  hivtrace_cluster_network_graph as clusterNetwork,
+  hivtrace_cluster_depthwise_traversal as computeCluster,
+};
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/clustersOfInterest.js.html b/docs/clustersOfInterest.js.html new file mode 100644 index 0000000..a34bcd6 --- /dev/null +++ b/docs/clustersOfInterest.js.html @@ -0,0 +1,1984 @@ + + + + + JSDoc: Source: clustersOfInterest.js + + + + + + + + + + +
+ +

Source: clustersOfInterest.js

+ + + + + + +
+
+
import * as d3 from "d3";
+import _ from "underscore";
+import { jsPanel } from "jspanel4";
+import autocomplete from "autocomplete.js";
+import * as timeDateUtil from "./timeDateUtil.js";
+import * as tables from "./tables.js";
+import * as helpers from "./helpers.js";
+import * as misc from "./misc.js";
+import { hivtrace_cluster_depthwise_traversal } from "./misc";
+import * as kGlobals from "./globals.js";
+
+let priority_set_editor = null;
+
+/**
+ * Initializes the component, setting up event listeners and UI elements.
+
+ * @param {Object} self - The component object itself.
+
+ * @returns {void}
+ */
+
+function init(self) {
+  if (self._is_CDC_ && self.isPrimaryGraph) {
+    let new_set = misc.get_ui_element_selector_by_role("new_priority_set");
+    if (new_set) {
+      window.addEventListener("beforeunload", (e) => {
+        if (self.priority_groups_pending() > 0) {
+          e.preventDefault();
+          return "There are clusters of interest that have not been confirmed. Closing the window now will not finalize their creation.";
+        }
+        return null;
+      });
+
+      d3.selectAll(new_set).on("click", (e) => {
+        open_editor(self, []);
+        self.redraw_tables();
+      });
+    }
+
+    let merge_sets = misc.get_ui_element_selector_by_role(
+      "merge_priority_sets"
+    );
+
+    if (merge_sets) {
+      d3.selectAll(merge_sets).on("click", (e) => {
+        $(misc.get_ui_element_selector_by_role("priority_set_merge")).modal();
+      });
+    }
+  }
+}
+
+/**
+ * Checks if a provided name for a priority group is valid.
+
+ * @param {Object[]} defined_priority_groups - An array of existing priority group objects.
+ * @param {string} string - The name to be validated.
+ * @param {string} [prior_name] (optional) - The previous name of the priority group (used for edit case).
+
+ * @returns {boolean} True if the name is valid, false otherwise.
+
+ * @description
+ * A valid name must:
+ *  - Have a length between 1 and 35 characters.
+ *  - Not be a duplicate among existing priority groups (excluding itself if editing).
+ */
+
+function priority_groups_check_name(
+  defined_priority_groups,
+  string,
+  prior_name
+) {
+  if (string.length) {
+    if (string.length >= 36) return false;
+    return !_.some(
+      defined_priority_groups,
+      (d) => d.name === string && d.name !== prior_name
+    );
+  }
+  return false;
+}
+
+/**
+ * Opens a priority node set editor.
+
+ * @param {Object} self - The main network visualization object.
+ * @param {Array} node_set - An existing priority node set (optional).
+ * @param {string} name - Name of the priority node set (optional for new sets).
+ * @param {string} description - Description of the priority node set (optional).
+ * @param {string} cluster_kind - The method used to identify the cluster (optional).
+ * @param {Array} kind_options - Available options for cluster identification methods.
+ * @param {string} validation_mode - Indicates the mode (create, validate, revise).
+ * @param {Object} existing_set - Reference to the existing priority node set (for revisions).
+ * @param {string} cluster_tracking - Method for tracking cluster growth (optional).
+ * @param {string} created_by - Who created the node set (system or manual).
+ */
+
+function open_editor(
+  self,
+  node_set,
+  name,
+  description,
+  cluster_kind,
+  kind_options,
+  validation_mode,
+  existing_set,
+  cluster_tracking,
+  created_by
+) {
+  /*
+      validation_mode could be
+        - null (create new set)
+        - "validate" (validate an automatically generated dataset)
+        - "revise" (revise a dataset)
+  */
+  if (priority_set_editor || !self.isPrimaryGraph) return;
+  // only open one editor at a time
+  // only primary network supports editor view
+
+  if (self._is_CDC_executive_mode) {
+    alert(kGlobals.network.WarnExecutiveMode);
+    return;
+  }
+
+  created_by = existing_set
+    ? existing_set.createdBy
+    : created_by || kGlobals.CDCCOICreatedManually;
+
+  priority_set_editor = jsPanel.create({
+    theme: "bootstrap-primary",
+    headerTitle: "Priority node set editor",
+    headerControls: { size: "lg", maximize: "remove" },
+    position: {
+      my: "center",
+      at: "center",
+      offsetX: 0,
+      offsetY: 0,
+    },
+    contentSize: {
+      width: function () {
+        return window.innerWidth * 0.8;
+      },
+      height: function () {
+        return window.innerHeight / 3;
+      },
+    },
+    content: "",
+    contentOverflow: "scroll",
+    callback: function () {
+      var panel_object = this;
+      panel_object.network_nodes = [];
+      panel_object.saved = false;
+      panel_object.prior_name =
+        validation_mode && validation_mode.length && existing_set
+          ? existing_set.name
+          : null;
+
+      panel_object.can_edit_kind = existing_set
+        ? existing_set.createdBy !== "System"
+        : true;
+
+      panel_object.can_edit_name = existing_set
+        ? existing_set.createdBy !== "System"
+        : true;
+
+      panel_object.can_edit_tracking = !existing_set;
+
+      panel_object.can_add = function (id) {
+        return !_.some(panel_object.network_nodes, (d) => d.id === id);
+      };
+
+      var panel_content = d3.select(panel_object.content);
+      panel_content.selectAll("*").remove();
+
+      var form = panel_content
+        .append("form")
+        .attr("action", "javascript:void(0);")
+        .classed("form-inline", true);
+
+      var form_grp = form.append("div").classed("form-group", true);
+
+      form_grp
+        .append("input")
+        .classed("form-control input-sm", true)
+        .attr("placeholder", "Add node by ID")
+        .attr("data-hivtrace-ui-role", "priority-panel-nodeids");
+
+      var submit_button = form
+        .append("button")
+        .classed("btn btn-primary btn-sm", true)
+        .attr("id", "priority-panel-add-node")
+        .attr("disabled", "disabled")
+        .on("click", (e) => {
+          panel_object.append_node();
+        });
+
+      submit_button.append("i").classed("fa fa-plus", true);
+
+      form
+        .append("p")
+        .classed("alert alert-warning", true)
+        .style("display", "inline")
+        .text(
+          "At this time, only nodes that cluster in the network at the 1.5% or 0.5% genetic distance threshold level are available for selection."
+        );
+      //var preview_grp = form.append ("div").classed ("form-group", true);
+
+      var form_save = panel_content
+        .append("form")
+        .classed("form", true)
+        .attr("action", "javascript:void(0);")
+        .style("display", "none");
+
+      var grp_name = form_save.append("div");
+
+      if (panel_object.prior_name) {
+        grp_name.classed("form-group has-success", true);
+      } else {
+        grp_name.classed("form-group has-error", true);
+      }
+
+      var grp_name_button = grp_name
+        .append("input")
+        .classed("form-control input-sm", true)
+        .attr("placeholder", "Name this cluster of interest")
+        .attr("data-hivtrace-ui-role", "priority-panel-name")
+        .attr("maxlength", 100);
+
+      var grp_name_box_label = grp_name
+        .append("p")
+        .classed("help-block", true)
+        .text("Name this cluster of interest");
+
+      var grp_kind = form_save.append("div").classed("form-group", true);
+
+      var grp_kind_select = grp_kind
+        .append("select")
+        .classed("form-control input-sm", true)
+        .attr("data-hivtrace-ui-role", "priority-panel-kind");
+
+      if (!panel_object.can_edit_kind) {
+        grp_kind_select.property("disabled", true);
+        grp_kind_select.attr(
+          "title",
+          "The method of cluster identification cannot be changed for system generated cluster of interest. However, after confirming this cluster of interest, you can clone it and then change this field as needed"
+        );
+      } else {
+        grp_kind_select.attr("title", null);
+      }
+
+      if (!panel_object.can_edit_name) {
+        grp_name_button.property("disabled", true);
+        grp_name_button.attr(
+          "title",
+          "The name cannot be changed for system generated cluster of interest. However, after confirming this cluster of interest, you can clone it and then change this field as needed"
+        );
+      } else {
+        grp_name_button.attr("title", null);
+      }
+
+      grp_kind_select
+        .selectAll("option")
+        .data(kind_options || kGlobals.CDCCOIKind)
+        .enter()
+        .insert("option")
+        .text((d) => d)
+        .property("selected", (d) => d === cluster_kind);
+
+      grp_kind
+        .append("p")
+        .classed("help-block", true)
+        .text("Cluster identification method");
+
+      var grp_tracking = form_save.append("div").classed("form-group", true);
+
+      var grp_tracking_select = grp_tracking
+        .append("select")
+        .classed("form-control input-sm", true)
+        .attr("data-hivtrace-ui-role", "priority-panel-tracking");
+
+      if (!panel_object.can_edit_tracking) {
+        grp_tracking_select.property("disabled", true);
+        grp_tracking_select.attr(
+          "title",
+          "The method of tracking cannot be changed for existing clusters (system generated or manual). However, you can clone this cluster of interest and then change this field as needed"
+        );
+      } else {
+        grp_tracking_select.attr("title", null);
+      }
+
+      cluster_tracking =
+        cluster_tracking || kGlobals.CDCCOITrackingOptionsDefault;
+
+      grp_tracking_select
+        .selectAll("option")
+        .data(kGlobals.CDCCOITrackingOptions)
+        .enter()
+        .insert("option")
+        .text((d) => d)
+        .property("selected", (d) => d === cluster_tracking);
+
+      grp_tracking
+        .append("p")
+        .classed("help-block", true)
+        .text("Method of tracking cluster of interest growth");
+
+      var grp_desc = form_save.append("div").classed("form-group", true);
+
+      grp_desc
+        .append("textarea")
+        .classed("form-control input-sm", true)
+        .attr("placeholder", "Cluster of Interest Description")
+        .attr("data-hivtrace-ui-role", "priority-panel-description")
+        .text(description);
+      grp_desc
+        .append("p")
+        .classed("help-block", true)
+        .text("Describe this cluster of interest");
+
+      panel_object.first_save = true;
+      panel_object.cleanup_attributes = function () {
+        _.each(self.nodes, (n) => {
+          _.each(
+            [
+              "_priority_set_fixed",
+              "_priority_set_date",
+              "_priority_set_kind",
+              "_priority_set_autoadded",
+            ],
+            (xtra) => {
+              delete n[xtra];
+            }
+          );
+        });
+      };
+
+      function is_node_editable(node) {
+        return !node["_priority_set_fixed"];
+      }
+
+      function is_node_deletable(node, cm) {
+        return (
+          cm === kGlobals.CDCCOICreatedManually || !node["_priority_set_fixed"]
+        );
+      }
+
+      let createdDate =
+        existing_set && validation_mode && validation_mode.length
+          ? existing_set.created
+          : timeDateUtil.getCurrentDate();
+
+      let modifiedDate =
+        validation_mode === "validate" &&
+        created_by === kGlobals.CDCCOICreatedBySystem
+          ? self.today
+          : timeDateUtil.getCurrentDate();
+
+      function save_priority_set() {
+        /**
+          handler for priority set save requests
+      */
+        form_save.style("display", null);
+
+        let res = true;
+
+        // check if can save (name set etc)
+        if (panel_object.network_nodes.length) {
+          let name, desc, kind, tracking;
+
+          [name, desc, kind, tracking] = _.map(
+            [
+              "priority-panel-name",
+              "priority-panel-description",
+              "priority-panel-kind",
+              "priority-panel-tracking",
+            ],
+            (k) =>
+              $(d3.select(misc.get_ui_element_selector_by_role(k)).node()).val()
+          );
+
+          if (
+            !panel_object.first_save &&
+            priority_groups_check_name(
+              self.defined_priority_groups,
+              name,
+              panel_object.prior_name
+            )
+          ) {
+            let set_description = {
+              name: name,
+              description: desc,
+              nodes: _.map(panel_object.network_nodes, (d) => ({
+                name: d.id,
+                added: d["_priority_set_date"],
+                kind: d["_priority_set_kind"],
+                autoadded: d["_priority_set_autoadded"],
+              })),
+              created: timeDateUtil.DateFormats[0](createdDate),
+              modified: timeDateUtil.DateFormats[0](modifiedDate),
+              kind: kind,
+              tracking: tracking,
+              createdBy: created_by,
+              expanded: false,
+              autocreated: existing_set ? existing_set.autocreated : false,
+              autoexpanded: existing_set ? existing_set.autoexpanded : false,
+              pending: false,
+            };
+
+            if (tracking !== kGlobals.CDCCOITrackingOptionsNone) {
+              let added_nodes = self.auto_expand_pg_handler(set_description);
+              if (added_nodes.size) {
+                if (
+                  confirm(
+                    'This cluster of interest does not include all the nodes in the current network that are eligible for membership by growth criterion  "' +
+                      tracking +
+                      '". These ' +
+                      added_nodes.size +
+                      " additional nodes will be automatically added to this cluster of interest when you save it. If you don’t want to add these nodes to the cluster of interest please select 'Cancel' and change the growth criterion."
+                  )
+                ) {
+                  _.each([...added_nodes], (nid) => {
+                    let n = self.json.Nodes[nid];
+                    set_description.nodes.push({
+                      name: n.id,
+                      added: timeDateUtil.getCurrentDate(),
+                      kind: kGlobals.CDCCOINodeKindDefault,
+                    });
+                  });
+                } else {
+                  return false;
+                }
+              }
+            }
+
+            let operation = null;
+            if (panel_object.prior_name) {
+              operation = existing_set.pending ? "insert" : "update";
+            }
+            res = priority_groups_add_set(
+              self,
+              set_description,
+              true,
+              true,
+              panel_object.prior_name,
+              operation
+            );
+            // clean up temporary flags from nodes
+            panel_object.saved = true;
+            panel_object.cleanup_attributes();
+            panel_object.close();
+            if (validation_mode === "validate") {
+              if (self.priority_set_table_writeable) {
+                let tab_pill = misc.get_ui_element_selector_by_role(
+                    "priority_set_counts"
+                  ),
+                  tab_pill_select = d3.select(tab_pill),
+                  remaining_sets = Number(tab_pill_select.text());
+                tab_pill_select.text(remaining_sets - 1);
+                d3.select("#banner_coi_counts").text(remaining_sets - 1);
+              }
+            }
+          }
+          panel_object.first_save = false;
+        }
+        let panel_to_focus = document.querySelector(
+          misc.get_ui_element_selector_by_role("priority-panel-name")
+        );
+        if (panel_to_focus) panel_to_focus.focus();
+        return res;
+      }
+
+      var save_set_button = form
+        .append("button")
+        .classed("btn btn-primary btn-sm pull-right", true)
+        .text(validation_mode === "validate" ? "Review & Save" : "Save")
+        .attr("disabled", "disabled")
+        .attr("id", "priority-panel-save")
+        .on("click", (e) => {
+          save_priority_set();
+        });
+
+      form
+        .append("button")
+        .classed("btn btn-info btn-sm pull-right", true)
+        .attr("id", "priority-panel-preview")
+        .text("Preview @1.5%")
+        .on("click", (e) => {
+          priority_set_view(self, priority_set_editor, {
+            "priority-edge-length": 0.015,
+            timestamp: createdDate,
+          });
+        });
+      form
+        .append("button")
+        .classed("btn btn-info btn-sm pull-right", true)
+        .attr("id", "priority-panel-preview-subcluster")
+        .text("Preview @" + self.subcluster_threshold * 100 + "%")
+        .on("click", (e) => {
+          priority_set_view(self, priority_set_editor, {
+            "priority-edge-length": self.subcluster_threshold,
+            timestamp: createdDate,
+          });
+        });
+
+      $(grp_name_button.node()).on("input propertychange", function (e) {
+        let current_text = $(this).val();
+        if (
+          priority_groups_check_name(
+            self.defined_priority_groups,
+            current_text,
+            panel_object.prior_name
+          )
+        ) {
+          grp_name.classed({
+            "has-success": true,
+            "has-error": false,
+            "has-warning": false,
+          });
+          grp_name_box_label.text("Name this cluster of interest");
+          if (panel_object.network_nodes.length) {
+            save_set_button.attr("disabled", null);
+          }
+        } else {
+          let too_long = current_text.length >= 36;
+          grp_name.classed({
+            "has-success": false,
+            "has-error": true,
+          });
+          let error_message = too_long
+            ? "MUST be shorter than 36 characters"
+            : "MUST be unique";
+          grp_name_box_label.text(
+            "Name this cluster of interest " + error_message
+          );
+          save_set_button.attr("disabled", "disabled");
+        }
+      });
+
+      if (name) {
+        grp_name_button.attr("value", name);
+        $(grp_name_button.node()).trigger("input");
+      }
+
+      var auto_object = autocomplete(
+        misc.get_ui_element_selector_by_role("priority-panel-nodeids"),
+        { hint: false },
+        [
+          {
+            source: function (query, callback) {
+              function escapeRegExp(string) {
+                return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+              }
+              var hits = [];
+              const pattern = new RegExp(escapeRegExp(query), "i");
+              for (
+                var i = 0;
+                hits.length < 10 && i < self.json["Nodes"].length;
+                i++
+              ) {
+                if (pattern.test(self.json["Nodes"][i].id)) {
+                  if (panel_object.can_add(self.json["Nodes"][i].id)) {
+                    hits.push(self.json["Nodes"][i].id);
+                  }
+                }
+              }
+              callback(hits);
+            },
+            templates: {
+              suggestion: function (suggestion) {
+                return suggestion;
+              },
+            },
+          },
+        ]
+      );
+
+      panel_object.validate_input = function (expression, skip_ui) {
+        expression = expression || auto_object.autocomplete.getVal();
+        const validator = _.filter(
+          self.json["Nodes"],
+          (n) => n.id === expression
+        );
+        if (validator.length === 1 && panel_object.can_add(validator[0].id)) {
+          if (!skip_ui) {
+            submit_button.attr("disabled", null);
+          }
+          return validator[0];
+        } else if (!skip_ui) {
+          submit_button.attr("disabled", "disabled");
+        }
+        return null;
+      };
+
+      panel_object._append_node = function (node) {
+        if (!("_priority_set_date" in node)) {
+          node["_priority_set_date"] = createdDate;
+        }
+        if (!("_priority_set_kind" in node)) {
+          node["_priority_set_kind"] = kGlobals.CDCCOINodeKindDefault;
+        }
+        panel_object.network_nodes.push(node);
+      };
+
+      panel_object.can_add_nodes = function () {
+        if (created_by !== kGlobals.CDCCOICreatedManually) {
+          alert(
+            "Cannot add nodes to system generated clusters of interest. You may clone this cluster of interest and then add nodes to it manually if necessary."
+          );
+          return false;
+        }
+        return true;
+      };
+
+      panel_object.append_node = function (id, skip_ui) {
+        if (!panel_object.can_add_nodes()) {
+          return;
+        }
+
+        var node_to_add = panel_object.validate_input(id, skip_ui);
+        if (node_to_add) {
+          panel_object._append_node(node_to_add);
+          panel_object.table_handler(panel_object);
+          panel_object.validate_input();
+        }
+      };
+
+      panel_object.append_nodes = function (nodes_to_add, existing_attributes) {
+        if (!panel_object.can_add_nodes()) {
+          return;
+        }
+        let existing_ids = {};
+
+        _.each(panel_object.network_nodes, (n) => {
+          existing_ids[n.id] = 1;
+        });
+
+        let need_update = false;
+        let valid_ids = {};
+        _.each(self.json["Nodes"], (n) => {
+          if (!existing_ids[n.id]) {
+            if (existing_attributes) {
+              valid_ids[n.id] = _.extend(n, existing_attributes[n.id]);
+            } else {
+              valid_ids[n.id] = n;
+            }
+          }
+        });
+
+        _.each(nodes_to_add, (n) => {
+          if (!(n in existing_ids) && n in valid_ids) {
+            panel_object._append_node(valid_ids[n]);
+            existing_ids[n] = 1;
+            need_update = true;
+          }
+        });
+
+        if (need_update) {
+          panel_object.table_handler(panel_object);
+        }
+      };
+
+      panel_object.append_node_objects = function (nodes_to_add) {
+        if (!panel_object.can_add_nodes()) {
+          return;
+        }
+
+        let existing_ids = {};
+
+        _.each(panel_object.network_nodes, (n) => {
+          existing_ids[n.id] = 1;
+        });
+
+        let need_update = false;
+
+        _.each(nodes_to_add, (n) => {
+          if (!(n.id in existing_ids)) {
+            panel_object._append_node(n);
+            existing_ids[n.id] = 1;
+            need_update = true;
+          }
+        });
+
+        if (need_update) {
+          panel_object.table_handler(panel_object);
+        }
+      };
+
+      panel_object.remove_node = function (n) {
+        panel_object.network_nodes = _.filter(
+          panel_object.network_nodes,
+          (nn) => nn !== n
+        );
+        panel_object.table_handler(panel_object);
+      };
+
+      auto_object
+        .on("autocomplete:selected", (event, suggestion, dataset, context) => {
+          auto_object.autocomplete.setVal(suggestion);
+          panel_object.validate_input();
+        })
+        .on("input propertychange", () => {
+          panel_object.validate_input();
+        });
+
+      panel_object.table_handler = function (panel) {
+        var table_container = panel_content.selectAll("table").data(["panel"]);
+        table_container.enter().append("table");
+        table_container
+          .classed(
+            "table table-striped table-condensed table-hover table-smaller",
+            true
+          )
+          .attr("id", "priority-panel-node-table");
+
+        panel.setHeaderTitle(
+          "clusterOI editor (" +
+            panel.network_nodes.length +
+            " nodes)" +
+            (validation_mode ? " [automatically created review] " : "")
+        );
+
+        save_set_button.attr(
+          "disabled",
+          panel.network_nodes.length ? null : "disabled"
+        );
+
+        var del_form_generator = function () {
+          return `<form class="form">
+  <div class="form-group">
+    <div class="input-group"> <textarea class="form-control input-sm" data-hivtrace-ui-role="priority-description-form"
+        cols="40" rows="3"></textarea> </div>
+  </div> <button data-hivtrace-ui-role="priority-description-dismiss" class="btn btn-sm btn-default">Cancel</button>
+  <button data-hivtrace-ui-role="priority-description-save" class="btn btn-sm btn-default">Delete</button>
+</form>`;
+        };
+
+        let extra_columns = [
+          {
+            prepend: true,
+            description: {
+              value: "Added",
+              help: "When was this person added to the cluster of interest?",
+            },
+            generator: function (node) {
+              return {
+                value: node,
+                callback: function (element, payload) {
+                  let this_cell = d3.select(element);
+                  if (payload["_priority_set_date"]) {
+                    if (payload["_priority_set_autoadded"]) {
+                      this_cell.style("color", "darkred");
+                    }
+                    if (!is_node_editable(payload)) {
+                      this_cell.text(
+                        timeDateUtil.DateViewFormatMMDDYYY(
+                          payload["_priority_set_date"]
+                        )
+                      );
+                    } else {
+                      this_cell
+                        .append("input")
+                        .attr("type", "date")
+                        .attr(
+                          "value",
+                          timeDateUtil.DateViewFormatSlider(
+                            payload["_priority_set_date"]
+                          )
+                        )
+                        .on("change", (e, d) => {
+                          try {
+                            payload["_priority_set_date"] =
+                              timeDateUtil.DateViewFormatSlider.parse(
+                                $(d3.event.target).val()
+                              );
+                          } catch {
+                            // do nothing
+                          }
+                        });
+                    }
+                  } else {
+                    this_cell.text("N/A");
+                  }
+                },
+              };
+            },
+          },
+          {
+            prepend: true,
+            description: {
+              value: "Person identification method",
+              help: "How was this person identified as part of this cluster of interest?",
+            },
+            generator: function (node) {
+              return {
+                value: node,
+                html: true,
+                actions: function (item, value) {
+                  if (is_node_editable(value)) {
+                    return [
+                      {
+                        //icon: "fa-caret-down",
+                        classed: { "btn-default": true },
+                        text: value["_priority_set_kind"], //.split(" ")[0],
+                        help: "How was this person identified?",
+                        dropdown: kGlobals.CDCCOINodeKind,
+                        action: function (button, menu_value) {
+                          value["_priority_set_kind"] = menu_value;
+                          button.text(
+                            value["_priority_set_kind"] //.split(" ")[0]
+                          );
+                        },
+                      },
+                    ];
+                  }
+                  return [];
+                },
+
+                callback: function (element, payload) {
+                  let this_cell = d3.select(element);
+                  if (!is_node_editable(payload)) {
+                    this_cell
+                      .append("abbr")
+                      .attr("title", payload["_priority_set_kind"])
+                      .text(payload["_priority_set_kind"] /*.split(" ")[0]*/);
+                  }
+                  return this_cell;
+                },
+              };
+            },
+          },
+          {
+            // delete object option
+            prepend: true,
+            description: {
+              value: "",
+              actions: [
+                {
+                  icon: "fa-trash",
+                  action: function (b, v) {
+                    // iterate through the table and remove shown nodes one at a time
+                    // checking that the row is shown to allow for filtering and such
+
+                    let remaining_nodes = new Set(panel.network_nodes);
+
+                    table_container
+                      .selectAll("tr")
+                      .filter(function (d) {
+                        return d3.select(this).style("display") !== "none";
+                      })
+                      .each(function (d) {
+                        d3.select(this)
+                          .selectAll("td:first-child > button")
+                          .each(function (d) {
+                            let this_node = d3.select(this).datum();
+                            if (is_node_editable(this_node)) {
+                              remaining_nodes.delete(this_node);
+                            }
+                          });
+                      });
+
+                    const leftovers = remaining_nodes.values();
+                    panel.network_nodes = [];
+                    for (let entry of leftovers) {
+                      panel.network_nodes.push(entry);
+                    }
+                    panel.table_handler(panel);
+                  },
+                },
+              ],
+            },
+            generator: function (node) {
+              return {
+                value: node,
+                callback: function (element, payload) {
+                  var this_cell = d3.select(element);
+                  if (!is_node_deletable(payload, created_by)) {
+                    this_cell
+                      .append("button")
+                      .classed("btn btn-default btn-xs", true)
+                      .style("margin-left", "1em")
+                      .datum(payload)
+                      .property("disabled", true)
+                      .append("i")
+                      .classed("fa fa-ban", true);
+                  } else {
+                    this_cell
+                      .append("button")
+                      .classed("btn btn-default btn-xs", true)
+                      .style("margin-left", "1em")
+                      .datum(payload)
+                      .on("click", function () {
+                        handle_inline_confirm(
+                          d3.select(this),
+                          del_form_generator,
+                          "Are you sure you wish to permanently delete this node from the cluster of interest?",
+                          (d) => {
+                            panel_object.remove_node(payload);
+                          },
+                          true
+                        );
+                        d3.event.preventDefault();
+                        //panel_object.remove_node(payload);
+                      })
+                      .append("i")
+                      .classed("fa fa-trash", true);
+                  }
+                },
+              };
+            },
+          },
+        ];
+        if (!self._is_CDC_auto_mode) {
+          extra_columns.splice(1, 1);
+        }
+
+        self.draw_extended_node_table(
+          panel.network_nodes,
+          table_container,
+          extra_columns
+        );
+      };
+
+      panel_object.content.style.padding = "5px";
+      panel_object.network_nodes = node_set;
+      // inject node attributes if available
+      if (validation_mode) {
+        // existing nodes cannot be deleted
+        _.each(panel_object.network_nodes, (n) => {
+          n["_priority_set_fixed"] = true;
+        });
+      }
+
+      if (existing_set) {
+        priority_set_inject_node_attibutes(
+          self,
+          panel_object.network_nodes,
+          existing_set.nodes
+        );
+      }
+
+      panel_object.table_handler(this);
+    },
+    dragit: {
+      containment: [50, 50, 100, 50],
+    },
+    resizeit: {
+      containment: [50, 50, 100, 50],
+    },
+    onbeforeclose: function () {
+      if (!this.saved) {
+        if (
+          confirm(
+            "Close cluster of interest editor? Unsaved changes will be lost."
+          )
+        ) {
+          if (existing_set) {
+            const existing_nodes = new Set(
+              _.map(existing_set.nodes, (n) => n.name)
+            );
+            existing_set.node_objects = _.filter(
+              existing_set.node_objects,
+              (n) => existing_nodes.has(n.id)
+            );
+          }
+          this.cleanup_attributes();
+          return true;
+        }
+        return false;
+      }
+      return true;
+    },
+    onclosed: function () {
+      priority_set_editor = null;
+      self.redraw_tables();
+    },
+  });
+}
+
+/**
+ * Handles inline confirmation popovers.
+
+ * @param {HTMLElement} this_button - The button element that triggers the popover.
+ * @param {Function} generator - A function that generates the HTML content for the popover body.
+ * @param {string} text - The initial text to display in the popover's text area (optional).
+ * @param {Function} action - A callback function to be executed when the user confirms the action. Takes the value from the text area as input.
+ * @param {boolean} disabled - A flag indicating if the text area should be disabled (optional).
+*/
+
+function handle_inline_confirm(this_button, generator, text, action, disabled) {
+  this_button = $(this_button.node());
+  if (this_button.data("popover_shown") !== "shown") {
+    const popover = this_button
+      .popover({
+        sanitize: false,
+        placement: "right",
+        container: "body",
+        html: true,
+        content: generator,
+        trigger: "manual",
+      })
+      .on("shown.bs.popover", function (e) {
+        var clicked_object = d3.select(this);
+        var popover_div = d3.select(
+          "#" + clicked_object.attr("aria-describedby")
+        );
+        var textarea_element = popover_div.selectAll(
+          misc.get_ui_element_selector_by_role("priority-description-form")
+        );
+        var button_element = popover_div.selectAll(
+          misc.get_ui_element_selector_by_role("priority-description-save")
+        );
+        textarea_element.text(text);
+        if (disabled) textarea_element.attr("disabled", true);
+        button_element.on("click", (d) => {
+          action($(textarea_element.node()).val());
+          d3.event.preventDefault();
+          this_button.click();
+        });
+        button_element = popover_div.selectAll(
+          misc.get_ui_element_selector_by_role("priority-description-dismiss")
+        );
+        button_element.on("click", (d) => {
+          d3.event.preventDefault();
+          this_button.click();
+        });
+      });
+
+    popover.popover("show");
+    this_button.data("popover_shown", "shown");
+    this_button.off("hidden.bs.popover").on("hidden.bs.popover", function () {
+      $(this).data("popover_shown", "hidden");
+    });
+  } else {
+    this_button.data("popover_shown", "hidden");
+    this_button.popover("destroy");
+  }
+}
+
+/**
+ * Generates a dropdown menu for actions on a cluster of interest (COI).
+
+ * @param {Object} self - The main network visualization object.
+ * @param {Object} pg - The cluster of interest data.
+ * @returns {Array} An array of dropdown menu options.
+*/
+
+function _action_drop_down(self, pg) {
+  let dropdown = _.flatten(
+    [
+      _.map([self.subcluster_threshold, 0.015], (threshold) => ({
+        label:
+          "View this cluster of interest at link distance of " +
+          kGlobals.formats.PercentFormatShort(threshold),
+        action: function (button, value) {
+          priority_set_view(self, pg, {
+            timestamp: pg.modified || pg.created,
+            priority_set: pg,
+            "priority-edge-length": threshold,
+            title: pg.name + " @" + kGlobals.formats.PercentFormat(threshold),
+          });
+        },
+      })),
+    ],
+    true
+  );
+
+  if (!self._is_CDC_executive_mode) {
+    dropdown.push({
+      label: "Clone this cluster of interest in a new editor panel",
+      action: function (button, value) {
+        let ref_set = self.priority_groups_find_by_name(pg.name);
+        let copied_node_objects = _.clone(ref_set.node_objects);
+        priority_set_inject_node_attibutes(self, copied_node_objects, pg.nodes);
+        open_editor(
+          self,
+          copied_node_objects,
+          "",
+          "Clone of " + pg.name,
+          ref_set.kind
+        );
+        self.redraw_tables();
+      },
+    });
+    if (pg.createdBy !== "System") {
+      dropdown.push({
+        label: "Delete this cluster of interest",
+        action: function (button, value) {
+          if (confirm("This action cannot be undone. Proceed?")) {
+            self.priority_groups_remove_set(pg.name, true);
+          }
+        },
+      });
+    }
+    dropdown.push({
+      label: "View nodes in this cluster of interest",
+      data: {
+        toggle: "modal",
+        target: misc.get_ui_element_selector_by_role("cluster_list"),
+        priority_set: pg.name,
+      },
+    });
+  }
+  dropdown.push({
+    label: "Modify this cluster of interest",
+    action: function (button, value) {
+      let ref_set = self.priority_groups_find_by_name(pg.name);
+
+      if (ref_set) {
+        /*if (ref_set.modified.getTime() > self.today.getTime()) {
+          if (
+            !confirm(
+              "Editing priority sets modified after the point at which this network was created is not recommended."
+            )
+          )
+            return;
+        }*/
+        open_editor(
+          self,
+          ref_set.node_objects,
+          ref_set.name,
+          ref_set.description,
+          ref_set.kind,
+          null,
+          "update",
+          ref_set,
+          ref_set.tracking
+        );
+        self.redraw_tables();
+      }
+    },
+  });
+
+  dropdown.push({
+    label: "View history over time",
+    action: function (button, value) {
+      let ref_set = self.priority_groups_find_by_name(pg.name);
+      let report = self.generate_coi_temporal_report(ref_set);
+      let container = self.open_exclusive_tab_view_aux(
+        null,
+        "History of " + pg.name,
+        {}
+      );
+      misc.coi_timeseries(
+        report,
+        d3.select("#" + container).style("padding", "20px"),
+        1000
+      );
+    },
+  });
+
+  return dropdown;
+}
+
+/**
+ * Draws a table of priority sets (clusters of interest).
+
+ * @param {Object} self - The main network visualization object.
+ * @param {HTMLElement} container - The HTML element where the table will be displayed (optional).
+ * @param {Array} priority_groups - An array of objects representing the priority sets (optional).
+*/
+
+function draw_priority_set_table(self, container, priority_groups) {
+  container = container || self.priority_set_table;
+  if (container) {
+    priority_groups = priority_groups || self.defined_priority_groups;
+    self.priority_groups_compute_node_membership();
+    self.priority_groups_compute_overlap(priority_groups);
+    var headers = [
+      [
+        {
+          value: "Type",
+          sort: function (c) {
+            return c.value;
+          },
+          help: "How was this cluster of interest created",
+          width: 50,
+        },
+        {
+          value: "Name",
+          sort: "value",
+          filter: true,
+          width: 325,
+          text_wrap: true,
+          help: "Cluster of interest name",
+        },
+        {
+          value: "Modified/created",
+          width: 180,
+          sort: function (c) {
+            return c.value[0];
+          },
+          help: "When was the cluster of interest created/last modified",
+        },
+        {
+          value: "Growth",
+          sort: "value",
+          help: "How growth is handled",
+          width: 100,
+          //text_wrap: true
+        },
+        {
+          value: "Size",
+          width: 100,
+          presort: "desc",
+          sort: function (c) {
+            c = c.value;
+            if (c) {
+              return c[1] + (c[2] ? 1e10 : 0) + (c[3] ? 1e5 : 0);
+            }
+            return 0;
+          },
+          help: "Number of nodes in the cluster of interest",
+        },
+        {
+          value: "Priority",
+          width: 60,
+          sort: "value",
+          help: "Does the cluster of interest continue to meet priority criteria?",
+        },
+        {
+          value: "DXs in last 12 mo.",
+          width: 50,
+          sort: "value",
+          help: "The number of cases in the cluster of interest diagnosed in the past 12 months",
+        },
+        {
+          value: "Overlap",
+          width: 140,
+          sort: function (c) {
+            c = c.value;
+            if (c) {
+              return c[1];
+            }
+            return 0;
+          },
+          help: "How many other ClusterOI have overlapping nodes with this ClusterOI, and (if overlapping ClusterOI exist) how many nodes in this ClusterOI overlap with ANY other ClusterOI?",
+        },
+        /*,
+          {
+            value: "Cluster",
+            sort: "value",
+            help: "Which cluster does the node belong to"
+          }*/
+      ],
+    ];
+
+    if (self._is_CDC_auto_mode) {
+      headers[0].splice(3, 0, {
+        value: "clusterOI identification method",
+        width: 100,
+        sort: function (c) {
+          return c.value;
+        },
+        help: "Method of cluster identification",
+      });
+    }
+
+    var edit_form_generator = function () {
+      return `<form class="form"> 
+                      <div class="form-group"> 
+                          <div class="input-group">
+                          <textarea class="form-control input-sm" data-hivtrace-ui-role = "priority-description-form" cols = "40" rows = "3"></textarea>
+                          </div>
+                      </div>
+                      <button data-hivtrace-ui-role = "priority-description-dismiss" class = "btn btn-sm btn-default">Dismiss</button>
+                      <button data-hivtrace-ui-role = "priority-description-save" class = "btn btn-sm btn-default">Save</button>
+                  </form>`;
+    };
+
+    var rows = [];
+    _.each(priority_groups, (pg) => {
+      var this_row = [
+        {
+          value: pg.createdBy,
+          html: true,
+          width: 50,
+          format: (value) =>
+            pg.createdBy === kGlobals.CDCCOICreatedBySystem
+              ? '<i class="fa fa-2x fa-desktop" title="' +
+                kGlobals.CDCCOICreatedBySystem +
+                '" data-text-export=' +
+                kGlobals.CDCCOICreatedBySystem +
+                "></i>"
+              : '<i class="fa fa-2x fa-user" title="' +
+                kGlobals.CDCCOICreatedManually +
+                '" data-text-export=' +
+                kGlobals.CDCCOICreatedManually +
+                "></i>",
+        },
+        {
+          value: pg.name,
+          width: 325,
+          help:
+            pg.description +
+            (pg.pending ? " (new, pending confirmation)" : "") +
+            (pg.expanded
+              ? " (" + pg.expanded + " new nodes; pending confirmation)"
+              : ""),
+          volatile: true,
+          format: (value) =>
+            "<div style = 'white-space: nowrap; overflow: hidden; text-overflow : ellipsis;'>" +
+            (pg.autocreated || pg.autoexpanded
+              ? (pg.autoexpanded
+                  ? '<span class="label label-default">Grew</span>'
+                  : '<span class="label label-danger">New</span>') +
+                "&nbsp;<span style = 'font-weight: 900;' data-text-export = '" +
+                value +
+                "'>" +
+                value +
+                "</span>"
+              : '<span data-text-export = "' +
+                value +
+                '">' +
+                value +
+                "</span>") +
+            "</div>",
+          html: true,
+          actions: [],
+        },
+        {
+          width: 180,
+          value: [pg.modified, pg.created],
+          format: function (value) {
+            let vs = _.map(value, (v) => timeDateUtil.DateViewFormat(v));
+
+            if (vs[0] !== vs[1]) {
+              return vs[0] + " / " + vs[1];
+            }
+            return vs[0];
+          },
+        },
+        {
+          width: 100,
+          //text_wrap: true,
+          value: pg.tracking,
+          format: function (value) {
+            return kGlobals.CDCCOIConciseTrackingOptions[value];
+          },
+        },
+        {
+          value: [
+            pg.node_objects.length,
+            _.filter(pg.nodes, (g) => self.priority_groups_is_new_node(g))
+              .length,
+            pg.createdBy === kGlobals.CDCCOICreatedBySystem && pg.pending,
+            pg.meets_priority_def,
+          ],
+          width: 100,
+          format: function (v) {
+            //console.log (pg);
+            if (v) {
+              return (
+                v[0] +
+                (v[1]
+                  ? ' <span title="Number of nodes added by the system since the last network update" class="label label-default">' +
+                    v[1] +
+                    " new</span>"
+                  : "")
+              );
+            }
+            return "N/A";
+          },
+          html: true,
+        },
+        {
+          width: 60,
+          value: pg.meets_priority_def ? "Yes" : "No",
+        },
+        {
+          width: 50,
+          value: pg.last12,
+        },
+        {
+          width: 140,
+          value: [
+            pg.overlap.sets,
+            pg.overlap.nodes,
+            pg.overlap.duplicate,
+            pg.overlap.superset,
+          ],
+          format: function (v) {
+            if (v) {
+              return (
+                String(v[0]) +
+                (v[1]
+                  ? ' <span title="Number of nodes in the overlap" class="label label-default pull-right">' +
+                    v[1] +
+                    " nodes</span>"
+                  : "") +
+                (v[2].length
+                  ? ' <span title="clusterOIs which are exact duplicates of this clusterOI: ' +
+                    v[2].join(", ") +
+                    '" class="label label-danger pull-right">' +
+                    v[2].length +
+                    " duplicate clusterOI</span>"
+                  : "") +
+                (v[3].length
+                  ? ' <span title="clusterOIs which contain this clusterOI: ' +
+                    v[3].join(", ") +
+                    '" class="label label-warning pull-right">Fully contained in ' +
+                    v[3].length +
+                    " clusterOI</span>"
+                  : "")
+              );
+            }
+            return "N/A";
+          },
+          html: true,
+          actions:
+            pg.overlap.sets === 0
+              ? []
+              : [
+                  {
+                    icon: "fa-eye",
+                    dropdown: [
+                      {
+                        label: "List overlaps",
+                        data: {
+                          toggle: "modal",
+                          target:
+                            misc.get_ui_element_selector_by_role(
+                              "overlap_list"
+                            ),
+                          priority_set: pg.name,
+                        },
+                      },
+                    ],
+                  },
+                ],
+        },
+      ];
+
+      if (self._is_CDC_auto_mode) {
+        this_row.splice(3, 0, {
+          value: pg.kind,
+          width: 100,
+          format: function (v) {
+            if (v) {
+              return v;
+              //"<abbr title = '" + v + "'>" + v.split(" ")[0] + "</abbr>"
+            }
+            return "N/A";
+          },
+          html: true,
+        });
+      }
+
+      if (pg.pending) {
+        // pending user review
+        this_row[1].actions = [
+          {
+            icon: "fa-eye",
+            help: "Review and adjust this cluster of interest",
+            action: function (button, value) {
+              let nodeset = self.priority_groups_find_by_name(value);
+              if (nodeset) {
+                if (get_editor()) {
+                  alert(
+                    "Cannot confirm a cluster of interest while an editor window is open"
+                  );
+                } else {
+                  open_editor(
+                    self,
+                    nodeset.node_objects,
+                    nodeset.name,
+                    nodeset.description,
+                    nodeset.kind,
+                    null,
+                    "validate",
+                    nodeset,
+                    pg.tracking,
+                    pg.createdBy
+                  );
+                  self.redraw_tables();
+                }
+              }
+            },
+          },
+        ];
+      } else {
+        this_row[1].actions = [_.clone(this_row[1].actions)];
+        this_row[1].actions[this_row[1].actions.length - 1].splice(
+          -1,
+          0,
+          {
+            icon: "fa-info-circle",
+            classed: { "view-edit-cluster": true },
+            help: "View/edit this cluster of interest",
+            dropdown: _action_drop_down(self, pg),
+            /*action: function (button, menu_value) {
+                console.log (menu_value);
+            }*/
+          },
+          {
+            icon: "fa-edit",
+            classed: { "btn-info": true },
+            help: "Edit description",
+            action: function (this_button, cv) {
+              handle_inline_confirm(
+                this_button,
+                edit_form_generator,
+                pg.description,
+                (d) => {
+                  self.priority_groups_edit_set_description(pg.name, d, true);
+                }
+              );
+            },
+          }
+        );
+        this_row[1].actions[this_row[1].actions.length - 1].splice(
+          -1,
+          0,
+          (button_group, value) => {
+            if (get_editor()) {
+              return {
+                icon: "fa-plus",
+                help: "Add nodes in this cluster of interest to the new cluster of interest",
+                action: function (button, value) {
+                  let nodeset = self.priority_groups_find_by_name(value);
+                  if (nodeset) {
+                    get_editor().append_node_objects(nodeset.node_objects);
+                  }
+                },
+              };
+            }
+            return null;
+          }
+        );
+      }
+      this_row[1].actions = _.flatten(this_row[1].actions);
+      //console.log (this_row[0]);
+      if (pg.not_in_network.length) {
+        this_row[2]["actions"] = [
+          {
+            text: String(pg.not_in_network.length) + " removed",
+            classed: { "btn-danger": true, disabled: true },
+            help:
+              "Nodes removed from the network: " + pg.not_in_network.join(", "),
+          },
+        ];
+      }
+      rows.push(this_row);
+    });
+
+    let has_required_actions = "";
+    /* let has_automatic = self.priority_groups_pending();
+    let has_expanded = self.priority_groups_expanded();
+
+    if (has_automatic + has_expanded) {
+      let labeler = (c, description, c2) => {
+        if (c) {
+          c2 = c2 ? " and " : "";
+          return c2 + c + " " + description;
+        }
+        return "";
+      };
+
+      has_required_actions =
+        '<div class="alert alert-info">There are ' +
+        "<span style = 'color: darkred'>" + labeler(has_automatic, "automatically created") + "</span>" +
+        "<span style = 'color: orange'>" + labeler(has_expanded, "automatically expanded", has_automatic) + "</span>" +
+        ' priority sets.</div>';
+    } else {
+      has_required_actions = "";
+    }*/
+
+    tables.add_a_sortable_table(
+      container,
+      headers,
+      rows,
+      true,
+      has_required_actions +
+        `Showing <span class="badge" data-hivtrace-ui-role="table-count-shown">--</span>/<span class="badge" data-hivtrace-ui-role="table-count-total">--</span> clusters of interest.
+          <button class = "btn btn-sm btn-warning pull-right" data-hivtrace-ui-role="priority-subclusters-export">Export to JSON</button>
+          <button class = "btn btn-sm btn-primary pull-right" data-hivtrace-ui-role="priority-subclusters-export-csv">Export to CSV</button>`,
+      get_editor()
+    );
+
+    d3.select(
+      misc.get_ui_element_selector_by_role("priority-subclusters-export")
+    ).on("click", (d) => {
+      helpers.export_json_button(
+        self.priority_groups_export(),
+        timeDateUtil.DateViewFormatSlider(self.today)
+      );
+    });
+    d3.select(
+      misc.get_ui_element_selector_by_role("priority-subclusters-export-csv")
+    ).on("click", (d) => {
+      helpers.export_csv_button(
+        self.priority_groups_export_nodes(),
+        "clusters-of-interest"
+      );
+    });
+    d3.select("#priority_set_table_download").on("click", (d) => {
+      helpers.export_csv_button(
+        self.priority_groups_export_sets(),
+        "clusters_of_interest_table"
+      );
+    });
+  }
+}
+
+/**
+ * Creates a subcluster view for a specific priority set.
+
+ * @param {Object} self - The main network visualization object.
+ * @param {Object} priority_set - The priority set object.
+ * @param {Object} options - Optional configuration options for the view.
+*/
+
+function priority_set_view(self, priority_set, options) {
+  options = options || {};
+
+  let nodes = priority_set.node_objects || priority_set.network_nodes;
+  let current_time = timeDateUtil.getCurrentDate();
+  let edge_length =
+    options["priority-edge-length"] || self.subcluster_threshold;
+  let reference_date = options["timestamp"] || self.today;
+  let title =
+    options["title"] ||
+    "clusterOI " + (priority_set.prior_name || priority_set.name || "unnamed");
+  let node_dates = {};
+
+  if (priority_set.nodes) {
+    _.each(priority_set.nodes, (nd) => {
+      node_dates[nd.name] = nd.added;
+    });
+  } else {
+    _.each(priority_set.network_nodes, (nd) => {
+      node_dates[nd.id] = nd["_priority_set_date"];
+    });
+  }
+
+  let nodeDates = {};
+  if (options.priority_set && options.priority_set.nodes) {
+    _.each(options.priority_set.nodes, (d) => {
+      nodeDates[d.name] = d.added;
+    });
+  }
+
+  _.each(nodes, (d) => {
+    //console.log (d);
+    d.priority_set = 1;
+    d._added_date = d.id in nodeDates ? nodeDates[d.id] : d._priority_set_date;
+    if (d._added_date)
+      d._added_date = timeDateUtil.DateViewFormatSlider(d._added_date);
+    else d._added_date = null;
+  });
+
+  let pgDates = _.sortBy(_.keys(_.groupBy(nodes, (d) => d._added_date)));
+
+  let node_set = _.flatten(
+    hivtrace_cluster_depthwise_traversal(
+      self.json["Nodes"],
+      self.json["Edges"],
+      (e) => e.length <= edge_length,
+      null,
+      nodes
+    )
+  );
+
+  let refDate = timeDateUtil.DateViewFormat(reference_date);
+
+  let dco = "fee8c8fdbb84e34a33";
+  let defColorsOther = d3.scale
+    .ordinal()
+    .range(_.map(_.range(0, dco.length, 6), (d) => "#" + dco.substr(d, 6)));
+
+  let maxColors = 4;
+  let dcpg = "7b3294c2a5cfa6dba0008837";
+  let defColorsPG = d3.scale
+    .ordinal()
+    .range(_.map(_.range(0, dcpg.length, 6), (d) => "#" + dcpg.substr(d, 6)));
+
+  let viewEnum = [];
+  let dateID = {};
+  _.each(pgDates, (d, i) => {
+    if (d) {
+      if (pgDates.length > maxColors) {
+        if (i < pgDates.length - maxColors) {
+          dateID[d] = 0;
+          return;
+        } else if (i === pgDates.length - maxColors) {
+          dateID[d] = viewEnum.length;
+          viewEnum.push(
+            "In cluster of interest (added on or before " + d + ")"
+          );
+          return;
+        }
+      }
+      dateID[d] = viewEnum.length;
+      viewEnum.push("In cluster of interest (added " + d + ")");
+    }
+  });
+
+  let priorityColorOffset = viewEnum.length;
+
+  viewEnum.push("Diagnosed and in network before " + refDate);
+  viewEnum.push(
+    "Diagnosed or in network on or after " +
+      refDate +
+      " [directly linked to cluster of interest]"
+  );
+  viewEnum.push(
+    "Diagnosed or in network on or after " +
+      refDate +
+      " [indirectly linked to cluster of interest]"
+  );
+  let viewEnumMissing = [...viewEnum, kGlobals.missing.label];
+
+  let viewEnumMissingColors = _.map(viewEnumMissing, (d, i) => {
+    if (d !== kGlobals.missing.label) {
+      if (i < priorityColorOffset) {
+        return defColorsPG(d);
+      }
+      return defColorsOther(d);
+    }
+    return "gray";
+  });
+
+  self
+    .view_subcluster(
+      -1,
+      node_set,
+      title,
+      {
+        skip_recent_rapid: true,
+        init_code: function (network) {
+          _.each(network.json.Edges, (e) => {
+            let other_node = null;
+            if (network.json.Nodes[e.target].priority_set === 1) {
+              other_node = network.json.Nodes[e.source];
+            } else if (network.json.Nodes[e.source].priority_set === 1) {
+              other_node = network.json.Nodes[e.target];
+            }
+            if (other_node && other_node.priority_set !== 1) {
+              other_node.priority_set = 2; // directly linked to a priority set node
+            }
+          });
+        },
+        "computed-attributes": {
+          date_added: {
+            depends: [timeDateUtil._networkCDCDateField],
+            label: "Date added to cluster of interest",
+            type: "Date",
+            map: function (node) {
+              return node.id in node_dates
+                ? node_dates[node.id]
+                : kGlobals.missing.label;
+            },
+          },
+          priority_set: {
+            depends: [timeDateUtil._networkCDCDateField],
+            label: "Cluster of Interest Status",
+            enum: viewEnum,
+            type: "String",
+            map: function (node) {
+              //console.log ("PS", node.id, node.priority_set);
+              if (node.priority_set === 1) {
+                if (node._added_date) {
+                  return viewEnum[dateID[node._added_date]];
+                }
+                return viewEnum[0];
+              }
+              if (
+                self.filter_by_date(
+                  reference_date,
+                  timeDateUtil._networkCDCDateField,
+                  current_time,
+                  node,
+                  true
+                )
+              ) {
+                if (node.priority_set === 2) {
+                  return viewEnum[priorityColorOffset + 1];
+                }
+                return viewEnum[priorityColorOffset + 2];
+              }
+              return viewEnum[priorityColorOffset];
+            },
+            color_scale: function () {
+              return d3.scale
+                .ordinal()
+                .domain(viewEnumMissing)
+                .range(viewEnumMissingColors);
+            },
+          },
+        },
+      },
+      null,
+      null,
+      edge_length
+    )
+    .handle_attribute_categorical("priority_set");
+
+  _.each(nodes, (d) => {
+    delete d.priority_set;
+  });
+}
+
+/**
+ * Adds a new priority set to the network visualization.
+
+ * @param {Object} self - The main network visualization object.
+ * @param {Object} nodeset - The object representing the new priority set.
+ * @param {boolean} update_table - Flag indicating whether to update the priority set table. (optional)
+ * @param {boolean} not_validated - Flag indicating whether to perform validation before adding. (optional)
+ * @param {string} prior_name - Optional name of an existing priority set to replace.
+ * @param {string} op_code - Optional operation code (defaults to "insert").
+
+ * @returns {boolean} True if the set was added successfully, false otherwise.
+*/
+
+function priority_groups_add_set(
+  self,
+  nodeset,
+  update_table,
+  not_validated,
+  prior_name,
+  op_code
+) {
+  function check_dup() {
+    if (
+      nodeset.name[0] === " " ||
+      nodeset.name[nodeset.name.length - 1] === " "
+    ) {
+      alert(
+        "Cluster of interest '" +
+          nodeset.name +
+          "' has spaces either at the beginning or end of the name. Secure HIV-TRACE does not allow names that start or end with spaces."
+      );
+      return true;
+    }
+    let my_nodes = new Set(_.map(nodeset.nodes, (d) => d.name));
+    return _.some(self.defined_priority_groups, (d) => {
+      if (d.nodes.length === my_nodes.size) {
+        const same_nodes =
+          d.nodes.filter((x) => my_nodes.has(x.name)).length === d.nodes.length;
+        if (same_nodes && d.tracking === nodeset.tracking) {
+          alert(
+            "Cluster of interest '" +
+              d.name +
+              "' has the same set of nodes and the same growth criterion as this new cluster of interest. Secure HIV-TRACE does not allow creating exact duplicates of clusters of interest."
+          );
+          return true;
+        } else if (same_nodes) {
+          let keep_duplicate = confirm(
+            "Warning! Cluster of interest '" +
+              d.name +
+              "' has the same set of nodes as this cluster of interest, but a different growth criterion'. Click 'OK' to create, or 'Cancel' to abort."
+          );
+          let is_duplicate = !keep_duplicate;
+          return is_duplicate;
+        }
+      }
+      return false;
+    });
+  }
+
+  op_code = op_code || "insert";
+  if (not_validated) {
+    self.priority_groups_validate([nodeset]);
+  }
+  if (prior_name) {
+    let prior_index = _.findIndex(
+      self.defined_priority_groups,
+      (d) => d.name === prior_name
+    );
+    if (prior_index >= 0) {
+      if (prior_name !== nodeset.name) {
+        self.priority_groups_update_node_sets(prior_name, "delete");
+        op_code = "insert";
+      }
+      self.defined_priority_groups[prior_index] = nodeset;
+    } else {
+      if (check_dup()) return false;
+      self.defined_priority_groups.push(nodeset);
+    }
+  } else {
+    if (check_dup()) return false;
+    self.defined_priority_groups.push(nodeset);
+  }
+  self.priority_groups_update_node_sets(nodeset.name, op_code);
+
+  if (update_table) {
+    draw_priority_set_table(self);
+  }
+
+  return true;
+}
+
+/**
+ * Injects priority set related attributes into network nodes.
+
+ * @param {Object} self - The main network visualization object.
+ * @param {Array} nodes - Array of network nodes.
+ * @param {Array} node_attributes - Array of priority set attributes for specific nodes (identified by name).
+*/
+
+function priority_set_inject_node_attibutes(self, nodes, node_attributes) {
+  let attr_by_id = {};
+  _.each(node_attributes, (n, i) => {
+    attr_by_id[n.name] = {
+      _priority_set_date: n.added || self.today,
+      _priority_set_kind: n.kind || kGlobals.CDCCOINodeKindDefault,
+      _priority_set_autoadded: n.autoadded || false,
+    };
+  });
+  _.each(nodes, (n) => {
+    if (n.id in attr_by_id) {
+      _.extend(n, attr_by_id[n.id]);
+    }
+  });
+}
+
+/**
+ * Gets the current priority set editor object.
+
+ * @returns {Object} The priority set editor object, or null if not open.
+ */
+
+function get_editor() {
+  return priority_set_editor;
+}
+
+export {
+  init,
+  open_editor,
+  priority_set_view,
+  draw_priority_set_table,
+  priority_set_inject_node_attibutes,
+  get_editor,
+};
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/colorPicker.js.html b/docs/colorPicker.js.html new file mode 100644 index 0000000..c709d62 --- /dev/null +++ b/docs/colorPicker.js.html @@ -0,0 +1,109 @@ + + + + + JSDoc: Source: colorPicker.js + + + + + + + + + + +
+ +

Source: colorPicker.js

+ + + + + + +
+
+
/**
+ * Creates a color picker input element with a label.
+
+ * @param {string} id - The ID for the color picker input element.
+ * @param {Function} colorizer - A colorizer function that maps values to colors.
+
+ * @returns {string} The HTML markup for the color picker input element with the label.
+ */
+
+function colorPickerInput(id, colorizer) {
+  // set onchange event after template is rendered and returned from this function.
+  let colorPicker = `<div class="col-lg-2 hivtrace-color-picker">
+    <input type="color" name="${id}" value="${colorizer.category(id)}">
+    <label for="${id}">${id}</label>
+  </div>`;
+
+  return colorPicker;
+}
+
+/**
+ * Creates a color picker input element with a label for continuous values.
+
+ * @param {string} id - The ID for the color picker input element.
+ * @param {string} color - The initial color value for the color picker.
+
+ * @returns {string} The HTML markup for the color picker input element with the label.
+*/
+
+function colorPickerInputContinuous(id, color) {
+  // set onchange event after template is rendered and returned from this function.
+  let colorPicker = `<div class="col-lg-2 hivtrace-color-picker">
+    <input type="color" name="${id.toLowerCase()}" value="${color}">
+    <label for="${id.toLowerCase()}">${id}</label>
+  </div>`;
+
+  return colorPicker;
+}
+
+/**
+ * Creates a color stop input element with a label.
+
+ * @param {string} id - The ID for the color stop input element.
+ * @param {number} number - The initial value for the color stop.
+
+ * @returns {string} The HTML markup for the color stop input element with the label.
+*/
+
+function colorStops(id, number) {
+  // set onchange event after template is rendered and returned from this function.
+  let colorStops = `<div class="col-lg-2 hivtrace-color-stops">
+    <label for="color-stop">${id}</label>
+    <input type="number" name="color-stop" value="${number}" min="0" max="20"">
+  </div>`;
+
+  return colorStops;
+}
+
+export { colorPickerInput, colorPickerInputContinuous, colorStops };
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/column_definitions.js.html b/docs/column_definitions.js.html new file mode 100644 index 0000000..459295a --- /dev/null +++ b/docs/column_definitions.js.html @@ -0,0 +1,382 @@ + + + + + JSDoc: Source: column_definitions.js + + + + + + + + + + +
+ +

Source: column_definitions.js

+ + + + + + +
+
+
/**
+    A collection of table column definitions 
+*/
+
+var d3 = require("d3"),
+  _ = require("underscore"),
+  clustersOfInterest = require("./clustersOfInterest.js"),
+  HTX = require("./hiv_tx_network.js"),
+  kGlobals = require("./globals.js");
+
+/**
+    Column definitions for rendered tables
+    Each column definition is object-based and has the following components
+    
+    description:  [this relates to the heading]   
+        value (text): the displayed name of the column
+        sort (function): a function which takes the value associated with the table cell, and returns a value upon which to sort the column
+        help (text):  the text to display in a popover when the user hovers over the column name
+    generator: 
+        a function that returns a data-driven definition of a cell
+        it takes as an argument the value that is associated with a cell
+        and returns an object with the following values
+            
+            html (bool): whether or not the returned value should be rendered as HTML (default is no, i.e. text)
+            value: what is the value associated with the cell
+            volatile (bool): if set, this cell will be re-rendered under operations which could modify how its displayed (e.g. is a CoI editor open)
+            format (function): how to render the cell value
+            actions (function): generate context-specific menus for the cell
+                                returns null (none) or a vector (definitions of actions)
+                                
+                    An action is an object with the following fields
+                        icon (text): use this font awesome icon 
+                        action (function): a function that takes the clicked button and the cell value and does something
+                        help (text): the help message to display on hover over the button
+                
+        
+*/
+
+/**
+ * Defines secure column definitions for HIV Trace subcluster data.
+
+ * @param {Object} self (optional) - The object containing context for calculations (presumably the component using this function).
+
+ * @returns {Array<Object>} An array of column definition objects. Each object has the following properties:
+ *   - `description`:
+ *     - `value`: (string) The human-readable name of the column.
+ *     - `sort`: (function) A function used to sort the column data.
+ *     - `presort` (string, optional): The default sort direction ("asc" or "desc").
+ *     - `help`: (string) Help text displayed when hovering over the column header.
+ *   - `generator`: (function) A function that generates the value and actions for each cluster based on the provided cluster object.
+ *     - The generator function receives the cluster object as an argument.
+ *     - It should return an object with the following properties:
+ *       - `html`: (boolean) Whether the column value should be rendered as HTML.
+ *       - `value`: (array|string) The actual data for the column.
+ *       - `volatile`: (boolean, optional) Whether the value needs to be recalculated frequently.
+ *       - `format`: (function, optional) A function used to format the column value for display.
+ *       - `actions`: (function, optional) A function used to generate actions for the column.
+ *         - The actions function receives two arguments:
+ *           - `item`: (object) The current cluster object.
+ *           - `value`: (array|string) The value of the column for the current cluster.
+ *         - It should return an array of action objects, each with the following properties:
+ *           - `icon`: (string) The icon class name to display for the action.
+ *           - `action`: (function) The function executed when the action is clicked.
+ *             - The action function receives two arguments:
+ *               - `button`: (jQuery object) The button element representing the action.
+ *               - `v`: (array|string) The value of the column for the current cluster.
+ *           - `help`: (string) The help text displayed when hovering over the action icon.
+ */
+
+function secure_hiv_trace_subcluster_columns(self) {
+  return [
+    /** definition for the column which shows the #of cases dx'ed within 36 months
+         the value is an array, which enumerates the number of connected components of the 0.5% subcluster, which are ALL within 36 month dx, 
+         so can be more than one.
+         
+         The only action is to add the nodes in this subcluster to a CoI editor if open 
+         
+         Accepts a _self_ argument for transitive closure
+         
+       */
+    {
+      description: {
+        value: "Cases dx within 36 months",
+        sort: function (c) {
+          return c.value.length ? c.value[0].length : 0;
+        },
+        help: "Number of cases diagnosed in the past 36 months connected only through cases diagnosed within the past 36 months",
+      },
+      generator: function (cluster) {
+        return {
+          html: true,
+          value: cluster.recent_nodes,
+          volatile: true,
+          format: function (v) {
+            v = v || [];
+            if (v.length) {
+              return _.map(v, (e) => e.length).join(", ");
+            }
+            return "";
+          },
+          actions: function (item, value) {
+            if (
+              !clustersOfInterest.get_editor() ||
+              cluster.recent_nodes.length === 0
+            ) {
+              return null;
+            }
+            return _.map(cluster.recent_nodes, (c) => {
+              const nodeset = new Set(c);
+              return {
+                icon: "fa-plus",
+                action: function (button, v) {
+                  if (clustersOfInterest.get_editor()) {
+                    clustersOfInterest
+                      .get_editor()
+                      .append_node_objects(
+                        _.filter(
+                          cluster.children,
+                          (n) => nodeset.has(n.id) && n.priority_flag > 0
+                        )
+                      );
+                  }
+                  return false;
+                },
+                help: "Add to cluster of interest",
+              };
+            });
+          },
+        };
+      },
+    },
+
+    /** definition for the column which shows the #of cases dx'ed within 12 months
+         the value is an array, which enumerates the number of connected components of the 0.5% subcluster, which connect through nodes dx'ed 36 month dx, so can be more than one.
+         
+         The actions are to add the nodes in this subcluster to a CoI editor if open, and to determine if the nodes in this set are already a part of the CoI.
+         
+       */
+
+    {
+      description: {
+        value: "Cases dx within 12 months",
+        //"value",
+        sort: function (c) {
+          const v = c.value || [];
+          return v.length > 0 ? v[0].length : 0;
+        },
+        presort: "desc",
+        help: "Number of cases diagnosed in the past 12 months connected only through cases diagnosed within the past 36 months",
+      },
+      generator: function (cluster) {
+        const definition = {
+          html: true,
+          value: cluster.priority_score,
+          volatile: true,
+          format: function (v) {
+            v = v || [];
+            if (v.length) {
+              var str = _.map(v, (c) => c.length).join(", ");
+              if (
+                v[0].length >= self.CDC_data["autocreate-priority-set-size"]
+              ) {
+                var color = "red";
+                return "<span style='color:" + color + "'>" + str + "</span>";
+              }
+              return str;
+            }
+            return "";
+          },
+        };
+
+        definition["actions"] = function (item, value) {
+          let result = [];
+
+          if (cluster.priority_score.length > 0) {
+            result = result.concat(
+              _.map(cluster.priority_score, (c) => ({
+                icon: "fa-question",
+                help:
+                  "Do some of these " +
+                  c.length +
+                  " nodes belong to a cluster of interest?",
+                action: function (this_button, cv) {
+                  const nodeset = new Set(c);
+                  this_button = $(this_button.node());
+                  if (this_button.data("popover_shown") !== "shown") {
+                    const popover = this_button
+                      .popover({
+                        sanitize: false,
+                        placement: "right",
+                        container: "body",
+                        html: true,
+                        content: HTX.HIVTxNetwork.lookup_form_generator,
+                        trigger: "manual",
+                      })
+                      .on("shown.bs.popover", function (e) {
+                        var clicked_object = d3.select(this);
+                        var popover_div = d3.select(
+                          "#" + clicked_object.attr("aria-describedby")
+                        );
+                        var list_element = popover_div.selectAll(
+                          self.get_ui_element_selector_by_role(
+                            "priority-membership-list",
+                            true
+                          )
+                        );
+
+                        list_element.selectAll("li").remove();
+                        let check_membership = _.filter(
+                          _.map(self.defined_priority_groups, (g) =>
+                            //console.log(g);
+                            [
+                              g.name,
+                              _.filter(g.nodes, (n) => nodeset.has(n.name))
+                                .length,
+                              _.filter(
+                                g.partitioned_nodes[1]["new_direct"],
+                                (n) => nodeset.has(n.id)
+                              ).length,
+                              _.filter(
+                                g.partitioned_nodes[1]["new_indirect"],
+                                (n) => nodeset.has(n.id)
+                              ).length,
+                            ]
+                          ),
+                          (gg) => gg[1] + gg[2] + gg[3] > 0
+                        );
+
+                        if (check_membership.length === 0) {
+                          check_membership = [
+                            [
+                              "No nodes belong to any cluster of interest or are linked to any of the clusters of interest.",
+                            ],
+                          ];
+                        } else {
+                          check_membership = _.map(check_membership, (m) => {
+                            let description = "";
+                            if (m[1]) {
+                              description += " " + m[1] + " nodes belong";
+                            }
+                            if (m[2]) {
+                              description +=
+                                (description.length ? ", " : " ") +
+                                m[2] +
+                                " nodes are directly linked @ " +
+                                kGlobals.formats.PercentFormatShort(
+                                  self.subcluster_threshold
+                                );
+                            }
+                            if (m[3]) {
+                              description +=
+                                (description.length ? ", " : " ") +
+                                m[3] +
+                                " nodes are indirectly linked @ " +
+                                kGlobals.formats.PercentFormatShort(
+                                  self.subcluster_threshold
+                                );
+                            }
+
+                            description +=
+                              " to cluster of interest <code>" +
+                              m[0] +
+                              "</code>";
+                            return description;
+                          });
+                        }
+                        list_element = list_element
+                          .selectAll("li")
+                          .data(check_membership);
+                        list_element.enter().insert("li");
+                        list_element.html((d) => d);
+                      });
+
+                    popover.popover("show");
+                    this_button.data("popover_shown", "shown");
+                    this_button
+                      .off("hidden.bs.popover")
+                      .on("hidden.bs.popover", function () {
+                        $(this).data("popover_shown", "hidden");
+                      });
+                  } else {
+                    this_button.data("popover_shown", "hidden");
+                    this_button.popover("destroy");
+                  }
+                },
+              }))
+            );
+          }
+
+          if (
+            clustersOfInterest.get_editor() &&
+            cluster.priority_score.length > 0
+          ) {
+            result = result.concat(
+              _.map(cluster.priority_score, (c) => {
+                const nodeset = new Set(c);
+                return {
+                  icon: "fa-plus",
+                  action: function (button, v) {
+                    if (clustersOfInterest.get_editor()) {
+                      clustersOfInterest
+                        .get_editor()
+                        .append_node_objects(
+                          _.filter(
+                            cluster.children,
+                            (n) =>
+                              nodeset.has(n.id) &&
+                              (n.priority_flag === 2 || n.priority_flag === 1)
+                          )
+                        );
+                    }
+                    return false;
+                  },
+                  help: "Add to cluster of interest",
+                };
+              })
+            );
+          }
+
+          return result;
+        };
+
+        return definition;
+      },
+    },
+  ];
+}
+
+module.exports = {
+  secure_hiv_trace_subcluster_columns,
+};
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/fonts/OpenSans-Bold-webfont.eot b/docs/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000..5d20d91 Binary files /dev/null and b/docs/fonts/OpenSans-Bold-webfont.eot differ diff --git a/docs/fonts/OpenSans-Bold-webfont.svg b/docs/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000..3ed7be4 --- /dev/null +++ b/docs/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Bold-webfont.woff b/docs/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000..1205787 Binary files /dev/null and b/docs/fonts/OpenSans-Bold-webfont.woff differ diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.eot b/docs/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 0000000..1f639a1 Binary files /dev/null and b/docs/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.svg b/docs/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 0000000..6a2607b --- /dev/null +++ b/docs/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.woff b/docs/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000..ed760c0 Binary files /dev/null and b/docs/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Italic-webfont.eot b/docs/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 0000000..0c8a0ae Binary files /dev/null and b/docs/fonts/OpenSans-Italic-webfont.eot differ diff --git a/docs/fonts/OpenSans-Italic-webfont.svg b/docs/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 0000000..e1075dc --- /dev/null +++ b/docs/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Italic-webfont.woff b/docs/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000..ff652e6 Binary files /dev/null and b/docs/fonts/OpenSans-Italic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Light-webfont.eot b/docs/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 0000000..1486840 Binary files /dev/null and b/docs/fonts/OpenSans-Light-webfont.eot differ diff --git a/docs/fonts/OpenSans-Light-webfont.svg b/docs/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000..11a472c --- /dev/null +++ b/docs/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Light-webfont.woff b/docs/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000..e786074 Binary files /dev/null and b/docs/fonts/OpenSans-Light-webfont.woff differ diff --git a/docs/fonts/OpenSans-LightItalic-webfont.eot b/docs/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000..8f44592 Binary files /dev/null and b/docs/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/docs/fonts/OpenSans-LightItalic-webfont.svg b/docs/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000..431d7e3 --- /dev/null +++ b/docs/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-LightItalic-webfont.woff b/docs/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000..43e8b9e Binary files /dev/null and b/docs/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Regular-webfont.eot b/docs/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000..6bbc3cf Binary files /dev/null and b/docs/fonts/OpenSans-Regular-webfont.eot differ diff --git a/docs/fonts/OpenSans-Regular-webfont.svg b/docs/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 0000000..25a3952 --- /dev/null +++ b/docs/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Regular-webfont.woff b/docs/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000..e231183 Binary files /dev/null and b/docs/fonts/OpenSans-Regular-webfont.woff differ diff --git a/docs/fonts/glyphicons-halflings-regular.eot b/docs/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a495..0000000 Binary files a/docs/fonts/glyphicons-halflings-regular.eot and /dev/null differ diff --git a/docs/fonts/glyphicons-halflings-regular.svg b/docs/fonts/glyphicons-halflings-regular.svg deleted file mode 100644 index 94fb549..0000000 --- a/docs/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/fonts/glyphicons-halflings-regular.ttf b/docs/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc6..0000000 Binary files a/docs/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/docs/fonts/glyphicons-halflings-regular.woff b/docs/fonts/glyphicons-halflings-regular.woff deleted file mode 100644 index 9e61285..0000000 Binary files a/docs/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/docs/fonts/glyphicons-halflings-regular.woff2 b/docs/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b5..0000000 Binary files a/docs/fonts/glyphicons-halflings-regular.woff2 and /dev/null differ diff --git a/docs/global.html b/docs/global.html index 5c2800f..6d28239 100644 --- a/docs/global.html +++ b/docs/global.html @@ -1,5552 +1,13477 @@ - - - - - Documentation Global + + + JSDoc: Global + + - - - - - - - + + -
+ -
- -
- - + +

Requires

- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + - - - - - - - - - - - - - - - - + + + + \ No newline at end of file diff --git a/docs/img/glyphicons-halflings-white.png b/docs/img/glyphicons-halflings-white.png deleted file mode 100644 index 3bf6484..0000000 Binary files a/docs/img/glyphicons-halflings-white.png and /dev/null differ diff --git a/docs/img/glyphicons-halflings.png b/docs/img/glyphicons-halflings.png deleted file mode 100644 index a996999..0000000 Binary files a/docs/img/glyphicons-halflings.png and /dev/null differ diff --git a/docs/index.html b/docs/index.html index 0f93f81..1ce7306 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,585 +1,65 @@ - - - - - Documentation Index + + + JSDoc: Home + + - - - - - - - - -
-
-
-
-
-
-

hivtrace-viz

-

- This repository contains the visualization code for HIV-TRACE. -

-

Dependencies

-
    -
  • Yarn or NPM
  • -
-

Development

-
git clone https://github.com/veg/hivtrace-viz.git
-cd hivtrace-viz
-yarn
-yarn develop
-
-

Navigate your browser to localhost:8273

-

Deployment

-

- The HIV-Trace webpage was formerly hosted using github-pages - at https://veg.github.io/hivtrace-viz/ but has been migrated - to using a pm2 process at hivtrace-viz.hyphy.org (see - veg/operations documentation for details). -

-

- The master branch of this repo should always be in sync with - what is published to NPM and is live on the production - website. With the exception of urgent bug fixes, all changes - to veg/master should be done via pull requests from - veg/develop. -

-
-
-
-
- -
- -
- -
-
-
- - - - - - - - - - - - - - - - - - - + + + + + + +
+ +

Home

+ + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/misc.js.html b/docs/misc.js.html new file mode 100644 index 0000000..99d189a --- /dev/null +++ b/docs/misc.js.html @@ -0,0 +1,1216 @@ + + + + + JSDoc: Source: misc.js + + + + + + + + + + +
+ +

Source: misc.js

+ + + + + + +
+
+
var d3 = require("d3"),
+  _ = require("underscore"),
+  helpers = require("./helpers.js");
+
+var hivtrace_generate_svg_polygon_lookup = {};
+
+_.each(_.range(3, 20), (d) => {
+  var angle_step = (Math.PI * 2) / d;
+  hivtrace_generate_svg_polygon_lookup[d] = _.map(_.range(1, d), (i) => [
+    Math.cos(angle_step * i),
+    Math.sin(angle_step * i),
+  ]);
+});
+
+/**
+ * Creates and returns an SVG polygon generator.
+ *
+ * @constructor
+ * @returns {Object} An object with methods to generate and manipulate an SVG polygon.
+ * @property {function} polygon - Generates the SVG path for the polygon.
+ * @property {function} sides - Gets or sets the number of sides of the polygon.
+ *   When called with a number > 2, sets the sides and returns the polygon generator.
+ *   When called without arguments, returns the current number of sides.
+ * @property {function} type - Returns the polygon generator function.
+ * @property {function} size - Gets or sets the size of the polygon.
+ *   When called with a number, sets the size and returns the polygon generator.
+ *   When called without arguments, returns the current size.
+ *
+ * @example
+ * const polygonGenerator = hivtrace_generate_svg_polygon();
+ * const path = polygonGenerator();
+ * polygonGenerator.sides(8);
+ * polygonGenerator.size(100);
+ *
+ * @requires lodash
+ * @requires hivtrace_generate_svg_polygon_lookup
+ */
+
+function hivtrace_generate_svg_polygon() {
+  var self = this;
+
+  self.polygon = function () {
+    var path = " M" + self.radius + " 0";
+
+    if (self.sides in hivtrace_generate_svg_polygon_lookup) {
+      path += hivtrace_generate_svg_polygon_lookup[self.sides]
+        .map(
+          (value) =>
+            " L" + self.radius * value[0] + " " + self.radius * value[1]
+        )
+        .join(" ");
+    } else {
+      var angle_step = (Math.PI * 2) / self.sides,
+        current_angle = 0;
+      for (let i = 0; i < self.sides - 1; i++) {
+        current_angle += angle_step;
+        path +=
+          " L" +
+          self.radius * Math.cos(current_angle) +
+          " " +
+          self.radius * Math.sin(current_angle);
+      }
+    }
+
+    path += " Z";
+    return path;
+  };
+
+  self.polygon.sides = function (attr) {
+    if (_.isNumber(attr) && attr > 2) {
+      self.sides = attr;
+      return self.polygon;
+    }
+
+    return self.sides;
+  };
+
+  self.polygon.type = function () {
+    return self.polygon;
+  };
+
+  self.polygon.size = function (attr) {
+    if (_.isNumber(attr)) {
+      self.size = attr;
+      self.radius = Math.sqrt(attr / Math.PI);
+      return self.polygon;
+    }
+
+    return self.size;
+  };
+
+  self.polygon.size(64);
+  self.sides = 6;
+
+  return self.polygon;
+}
+
+/**
+ * Generates an SVG symbol based on the specified type.
+ *
+ * @param {string} type - The type of symbol to generate.
+ *   Possible values are:
+ *   - "circle"
+ *   - "cross"
+ *   - "diamond"
+ *   - "square"
+ *   - "triangle-down"
+ *   - "triangle-up"
+ *   - "triangle"
+ *   - "pentagon"
+ *   - "hexagon"
+ *   - "septagon"
+ *   - "octagon"
+ *   - "ellipse"
+ *
+ * @returns {Object} A D3 symbol generator or a custom polygon/ellipse generator.
+ *   - For "circle", "cross", "diamond", "square", "triangle-down", "triangle-up":
+ *     Returns a D3 symbol generator of the specified type.
+ *   - For "triangle", "pentagon", "hexagon", "septagon", "octagon":
+ *     Returns a custom polygon generator with the specified number of sides.
+ *   - For "ellipse":
+ *     Returns a custom ellipse generator.
+ *   - For any other input:
+ *     Returns a D3 symbol generator of type "circle" as default.
+ *
+ * @requires d3
+ * @requires hivtrace_generate_svg_polygon
+ * @requires hivtrace_generate_svg_ellipse
+ */
+
+function hivtrace_generate_svg_symbol(type) {
+  switch (type) {
+    case "circle":
+    case "cross":
+    case "diamond":
+    case "square":
+    case "triangle-down":
+    case "triangle-up":
+      return d3.svg.symbol().type(type);
+    case "triangle":
+      return new hivtrace_generate_svg_polygon().sides(3);
+    case "pentagon":
+      return new hivtrace_generate_svg_polygon().sides(5);
+    case "hexagon":
+      return new hivtrace_generate_svg_polygon().sides(6);
+    case "septagon":
+      return new hivtrace_generate_svg_polygon().sides(7);
+    case "octagon":
+      return new hivtrace_generate_svg_polygon().sides(8);
+    case "ellipse":
+      return new hivtrace_generate_svg_ellipse();
+    default:
+      return d3.svg.symbol().type("circle");
+  }
+}
+
+/**
+ * Computes the degree of each node in a graph.
+ *
+ * @param {Object} obj - The graph object containing nodes and edges.
+ * @param {Object} obj.Nodes - An object representing the nodes of the graph.
+ * @param {Object} obj.Edges - An object representing the edges of the graph.
+ * @param {string} obj.Edges[].source - The source node of an edge.
+ * @param {string} obj.Edges[].target - The target node of an edge.
+ *
+ * @description
+ * This function modifies the input object by adding a 'degree' property to each node.
+ * The degree of a node is the number of edges connected to it.
+ *
+ * @example
+ * const graph = {
+ *   Nodes: {
+ *     "1": {},
+ *     "2": {}
+ *   },
+ *   Edges: {
+ *     "e1": { source: "1", target: "2" }
+ *   }
+ * };
+ * hivtrace_compute_node_degrees(graph);
+ * // graph.Nodes["1"].degree === 1
+ * // graph.Nodes["2"].degree === 1
+ */
+
+function hivtrace_compute_node_degrees(obj) {
+  var nodes = obj.Nodes,
+    edges = obj.Edges;
+
+  for (var n in nodes) {
+    nodes[n].degree = 0;
+  }
+
+  for (var e in edges) {
+    nodes[edges[e].source].degree++;
+    nodes[edges[e].target].degree++;
+  }
+}
+
+/**
+ * Creates a download button for exporting table data to a text file.
+ *
+ * @param {string} parent_id - The selector for the parent element where the button will be appended.
+ * @param {string} table_id - The selector for the table to be exported.
+ * @param {boolean} csv - If true, exports as CSV; if false, exports as TSV.
+ * @param {string} [file_name_placeholder] - The base name for the exported file. If not provided, it defaults to the table_id without the first character.
+ * @returns {d3.Selection} The D3 selection of the created button.
+ *
+ * @requires d3
+ * @requires helpers
+ *
+ * @description
+ * This function creates a download button that, when clicked, exports the data from the specified table
+ * as either a CSV or TSV file. It uses D3 for DOM manipulation and assumes the existence of a `helpers`
+ * object with `table_to_text` and `export_handler` methods.
+ *
+ * @example
+ * hiv_trace_export_table_to_text("#parent-div", "#data-table", true, "export-data");
+ */
+
+function hiv_trace_export_table_to_text(
+  parent_id,
+  table_id,
+  csv,
+  file_name_placeholder
+) {
+  var the_button = d3.select(parent_id);
+  the_button.selectAll("[data-type='download-button']").remove();
+
+  the_button = the_button
+    .append("a")
+    .attr("target", "_blank")
+    .attr("data-type", "download-button")
+    .on("click", function (data, element) {
+      d3.event.preventDefault();
+      var table_tag = d3.select(this).attr("data-table");
+      var table_text = helpers.table_to_text(table_tag, csv ? "," : "\t");
+      file_name_placeholder = file_name_placeholder || table_tag.substring(1);
+      if (!csv) {
+        helpers.export_handler(
+          table_text,
+          file_name_placeholder + ".tsv",
+          "text/tab-separated-values"
+        );
+      } else {
+        helpers.export_handler(
+          table_text,
+          file_name_placeholder + ".csv",
+          "text/comma-separated-values"
+        );
+      }
+    })
+    .attr("data-table", table_id);
+
+  the_button.append("i").classed("fa fa-download fa-2x", true);
+  return the_button;
+}
+
+/**
+ * Generates a time series visualization for a cluster of interest (COI).
+ *
+ * @param {Object} cluster - The cluster object containing node and event information.
+ * @param {Object} cluster.node_info - Information about nodes in the cluster.
+ * @param {Object} cluster.event_info - Information about events in the cluster.
+ * @param {d3.Selection} element - The D3 selection of the element where the visualization will be rendered.
+ * @param {number} [plot_width=1000] - The width of the plot in pixels.
+ *
+ * @description
+ * This function creates a detailed time series visualization for a cluster of interest (COI).
+ * It displays nodes and events over time, with interactive features for highlighting
+ * and displaying additional information.
+ *
+ * The visualization includes:
+ * - A time axis
+ * - Lines representing nodes
+ * - Circles representing events
+ * - Interactive highlighting and tooltips
+ * - Time range boxes for context
+ *
+ * @requires d3
+ * @requires lodash
+ *
+ * @example
+ * const cluster = {
+ *   node_info: {...},
+ *   event_info: {...}
+ * };
+ * const element = d3.select("#visualization-container");
+ * hivtrace_coi_timeseries(cluster, element, 1200);
+ */
+
+function hivtrace_coi_timeseries(cluster, element, plot_width) {
+  const margin = { top: 30, right: 60, bottom: 10, left: 120 };
+  const formatTime = d3.time.format("%Y-%m-%d");
+  let data = _.sortBy(
+    _.map(cluster.node_info, (d) => [d[0], formatTime.parse(d[1])]),
+    (d) => d[1]
+  );
+  const barHeight = 15;
+  const height =
+    Math.ceil((data.length + 0.1) * barHeight) + margin.top + margin.bottom;
+  const events = _.map(cluster.event_info, (d, i) => [i, formatTime.parse(i)]);
+  const x_range = d3.extent(
+    _.map(data, (d) => d[1]).concat(_.map(events, (d) => d[1]))
+  );
+
+  plot_width = plot_width || 1000;
+
+  let x = d3.time
+    .scale()
+    .domain(x_range)
+    .rangeRound([margin.left, plot_width - margin.right]);
+
+  let y = d3.scale
+    .ordinal()
+    .domain(d3.range(data.length + 1))
+    .rangeRoundPoints([margin.top, height - margin.bottom], 0.1);
+
+  let x_axis_object = d3.svg
+    .axis()
+    .scale(x)
+    .orient("top")
+    .ticks(plot_width / 80)
+    .tickFormat(d3.time.format("%m/%y"));
+
+  element.selectAll("svg").remove();
+
+  const svg = element
+    .append("svg")
+    .attr("width", plot_width)
+    .attr("height", height)
+    .attr("viewBox", [0, 0, plot_width, height]);
+
+  svg
+    .append("g")
+    .attr("transform", "translate(0," + 0.6 * margin.top + ")")
+    .attr("class", "y time_axis")
+    //.style ("shape-rendering","crispEdges").style ("font-family", "sans-serif").style ("font-size","8").style ("fill", "none").style("stroke","black")
+    .call(x_axis_object)
+    .call((g) => g.select(".domain").remove());
+
+  svg
+    .append("g")
+    .attr("stroke", "#ddd")
+    .attr("stroke-width", 2)
+    .attr("opacity", 0.8)
+    .selectAll("line")
+    .data(events)
+    .enter()
+    .append("line")
+    .attr("x1", (d) => x(d[1]))
+    .attr("x2", (d) => x(d[1]))
+    .attr("y1", (d, i) => y(0))
+    .attr("y2", (d, i) => y(data.length));
+
+  let lines = svg
+    .append("g")
+    .selectAll("line")
+    .data(data)
+    .enter()
+    .append("line")
+    .attr("stroke", "#aaa")
+    .attr("stroke-width", 2)
+    .attr("x1", (d) => x(x_range[1]))
+    .attr("x2", (d) => x(d[1]))
+    .attr("y1", (d, i) => y(i))
+    .attr("y2", (d, i) => y(i));
+
+  let time_boxes = [null, null];
+  let highlight_nodes = new Set();
+
+  let titles = data.concat([["Nat'l priority", x_range[0]]]);
+  let text_labels = svg
+    .append("g")
+    .attr("font-family", "sans-serif")
+    .attr("font-size", 10)
+    .attr("font-weight", 700)
+    .selectAll("text")
+    .data(titles)
+    .enter()
+    .append("text")
+    .attr("text-anchor", "end")
+    .attr("x", (d) => x(d[1]))
+    .attr("y", (d, i) => y(i) + y.rangeBand() / 2)
+    .attr("dy", "0.35em")
+    .attr("dx", "-0.25em")
+    .attr("fill", "black")
+    .text((d) =>
+      d[0].indexOf(" ") < 0 ? _.map(d[0], (c) => "â–ˆ").join("") : d[0]
+    );
+
+  svg
+    .append("g")
+    .selectAll("circle")
+    .data(data)
+    .enter()
+    .append("circle")
+    .attr("fill", "black")
+    .attr("stroke", "black")
+    .attr("cx", (d) => x(d[1]))
+    .attr("cy", (d, i) => y(i))
+    .attr("r", 1);
+
+  svg
+    .append("g")
+    .selectAll("circle")
+    .data(events)
+    .enter()
+    .append("circle")
+    .attr("fill", (d, i) =>
+      _.some(cluster.event_info[d[0]].national_priority)
+        ? "firebrick"
+        : "steelblue"
+    )
+    .attr("stroke", "black")
+    .attr("cx", (d) => x(d[1]))
+    .attr("cy", (d, i) => y(data.length))
+    .attr(
+      "r",
+      (d) => 2 + Math.sqrt(d3.sum(cluster.event_info[d[0]].connected_componets))
+    )
+    .on("mouseover", (d, e) => {
+      const ed = cluster.event_info[d[0]];
+      _.each(ed.national_priority, (d, i) => {
+        if (d) {
+          _.each(ed.priority_nodes[i], (n) => highlight_nodes.add(n));
+        }
+      });
+      //console.log (highlight_nodes);
+      let years_ago = _.map([1, 3], (ya) => {
+        let some_years_ago = new Date(d[1]);
+        some_years_ago.setFullYear(d[1].getFullYear() - ya);
+        if (some_years_ago < x_range[0]) some_years_ago = x_range[0];
+        return some_years_ago;
+      });
+
+      let fills = ["firebrick", "grey"];
+      time_boxes = _.map(years_ago, (sya, i) =>
+        svg
+          .append("g")
+          .selectAll("rect")
+          .data([d])
+          .enter()
+          .append("rect")
+          .attr("fill", fills[i])
+          .attr("x", (d) => x(sya))
+          .attr("y", (d) => y(0))
+          .attr("width", x(d[1]) - x(sya))
+          .attr("height", (d) => -y(0) + y(data.length - 1))
+          .attr("opacity", 0.25)
+      );
+
+      lines
+        .attr("stroke-width", (d) => (highlight_nodes.has(d[0]) ? 5 : 2))
+        .attr("stroke", (d) => (highlight_nodes.has(d[0]) ? "black" : "#aaa"));
+      text_labels.attr("fill", (d) =>
+        highlight_nodes.has(d[0]) ? "firebrick" : "black"
+      );
+    })
+    .on("mouseout", (e, d) => {
+      lines.attr("stroke-width", 2).attr("stroke", "#aaa");
+      text_labels.attr("fill", "black");
+      highlight_nodes = new Set();
+      _.each(time_boxes, (box) => (box ? box.remove() : 0));
+      time_boxes = [null, null];
+    })
+    .append("title")
+    .text((d) => {
+      const ed = cluster.event_info[d[0]];
+      let text = d[0] + ". ";
+      if (_.some(ed.national_priority)) {
+        text += "National priority clusterOI. ";
+      }
+      text +=
+        String(d3.sum(ed.connected_componets)) +
+        " nodes in " +
+        ed.connected_componets.length +
+        " components. ";
+      text +=
+        "A total of " +
+        d3.sum(ed.priority_nodes, (d) => d.length) +
+        " nodes dx'ed in the previous 12 months; maximum of " +
+        d3.max(ed.priority_nodes, (d) => d.length) +
+        " in a subcluster";
+      return text;
+    });
+}
+
+/**
+ * Performs a depth-wise traversal on a cluster of nodes, considering edges and optional filters.
+ *
+ * @param {Object[]} nodes - An array of node objects. Each node should have an `id` property.
+ * @param {Object[]} edges - An array of edge objects. Each edge should have `source` and `target` properties
+ *   referencing node IDs.
+ * @param {Function} [edge_filter] - An optional filtering function applied to edges before traversal.
+ *   The function should accept an edge object and return a boolean indicating whether to include the edge.
+ * @param {Function} [save_edges] - An optional function used to store traversed edges. It should be called with
+ *   an array where each element represents the edges within a cluster.
+ * @param {Object[]} [seed_nodes] - An optional array of node objects to use as starting points for traversal.
+ *   If not provided, all nodes will be considered.
+ * @param {Set} [white_list] - An optional set of node IDs restricting traversal to nodes within the set.
+ *
+ * @throws {Error} If an edge references non-existent nodes.
+ *
+ * @returns {Object[][]} An array of clusters, where each cluster is an array of node objects.
+ */
+
+function hivtrace_cluster_depthwise_traversal(
+  nodes,
+  edges,
+  edge_filter,
+  save_edges,
+  seed_nodes,
+  white_list
+  // an optional set of node IDs (a subset of 'nodes') that will be considered for traversal
+  // it is further assumed that seed_nodes are a subset of white_list, if the latter is specified
+) {
+  var clusters = [],
+    adjacency = {},
+    by_node = {};
+
+  seed_nodes = seed_nodes || nodes;
+
+  _.each(nodes, (n) => {
+    n.visited = false;
+    adjacency[n.id] = [];
+  });
+
+  if (edge_filter) {
+    edges = _.filter(edges, edge_filter);
+  }
+
+  if (white_list) {
+    edges = _.filter(
+      edges,
+      (e) =>
+        white_list.has(nodes[e.source].id) && white_list.has(nodes[e.target].id)
+    );
+  }
+
+  _.each(edges, (e) => {
+    try {
+      adjacency[nodes[e.source].id].push([nodes[e.target], e]);
+      adjacency[nodes[e.target].id].push([nodes[e.source], e]);
+    } catch {
+      throw Error(
+        "Edge does not map to an existing node " + e.source + " to " + e.target
+      );
+    }
+  });
+
+  var traverse = function (node) {
+    if (!(node.id in by_node)) {
+      clusters.push([node]);
+      by_node[node.id] = clusters.length - 1;
+      if (save_edges) {
+        save_edges.push([]);
+      }
+    }
+    node.visited = true;
+
+    _.each(adjacency[node.id], (neighbor) => {
+      if (!neighbor[0].visited) {
+        by_node[neighbor[0].id] = by_node[node.id];
+        clusters[by_node[neighbor[0].id]].push(neighbor[0]);
+        if (save_edges) {
+          save_edges[by_node[neighbor[0].id]].push(neighbor[1]);
+        }
+        traverse(neighbor[0]);
+      }
+    });
+  };
+
+  _.each(seed_nodes, (n) => {
+    if (!n.visited) {
+      traverse(n);
+    }
+  });
+
+  return clusters;
+}
+
+/**
+ * Determines the type of an edge based on its length and predefined edge types.
+
+ * @param {Object} e - The edge object to be classified.
+ * @param {string[]} edge_types - An array of two edge types. The first type is used for edges shorter than or equal to `T`,
+ *   and the second type is used for edges longer than `T`.
+ * @param {number} T - The threshold value for edge length classification.
+
+ * @returns {string} The edge type corresponding to the edge's length.
+ */
+
+function edge_typer(e, edge_types, T) {
+  return edge_types[e.length <= T ? 0 : 1];
+}
+
+/**
+ * Generates a random ID string using a specified alphabet and length.
+
+ * @param {string[]} [alphabet] - An optional array of characters to use in the ID. If not provided, a default alphabet of letters "a" to "g" is used.
+ * @param {number} [length] - An optional length for the ID. If not provided, a default length of 32 is used.
+
+ * @returns {string} A randomly generated ID string.
+ */
+
+function random_id(alphabet, length) {
+  alphabet = alphabet || ["a", "b", "c", "d", "e", "f", "g"];
+  length = length || 32;
+  var s = "";
+  for (var i = 0; i < length; i++) {
+    s += _.sample(alphabet);
+  }
+  return s;
+}
+
+/**
+ * Generates a plot visualizing cluster dynamics over time.
+ *
+ * @param {Object[]} time_series - An array of data points, each with a required `time` property (a date object)
+ *   and optional properties representing attributes.
+ * @param {d3.selection} container - A D3 selection representing the container element for the plot.
+ * @param {string} x_title - The title for the x-axis.
+ * @param {string} y_title - The title for the y-axis.
+ * @param {d3.scale} [y_scale] - An optional D3 scale for the y-axis. If not provided, a linear scale will be used.
+ * @param {Function} [bin_by] - An optional function used to bin data points into time intervals.
+ *   The function should accept a date object and return an array with three elements:
+ *   - The bin label (e.g., "Q1 2023").
+ *   - The start date of the bin.
+ *   - The middle date of the bin (used for x-axis positioning).
+ *   If not provided, a default function that bins by quarters is used.
+ * @param {Object} [options] - An optional configuration object for the plot.
+ *   - `base_line`: (number, default: 20) The baseline value for the y-axis.
+ *   - `top`: (number, default: 40) The top padding for the plot.
+ *   - `right`: (number, default: 30) The right padding for the plot.
+ *   - `bottom`: (number, default: 60) The bottom padding for the plot.
+ *   - `left`: (number, default: 100) The left padding for the plot.
+ *   - `font_size`: (number, default: 18) The font size for labels and text elements.
+ *   - `rect_size`: (number, default: 22) The size of rectangles used in the legend.
+ *   - `width`: (number, default: 1024) The width of the plot container.
+ *   - `height`: (number, default: 600) The height of the plot container.
+ *   - `barchart`: (boolean, default: false) If true, the plot will be displayed as a bar chart.
+ *   - `skip_cumulative`: (boolean, default: false) If true, the cumulative area will not be displayed.
+ *   - `x-tick-format`: (Function) An optional function for formatting x-axis tick labels.
+ *   - `prefix`: (string) An optional prefix to add to attribute names displayed in the legend.
+ *   - `colorizer`: (Object) An optional colorizer object for attributes. Keys should be attribute names,
+ *     and values should be D3 scales used for coloring lines/bars.
+ *   - `drag`: (Object) An optional drag object for enabling dragging the plot.
+ *
+ * @throws {Error} If no data points are provided.
+ */
+
+function hivtrace_plot_cluster_dynamics(
+  time_series,
+  container,
+  x_title,
+  y_title,
+  y_scale,
+  bin_by,
+  options
+) {
+  options = options || {
+    base_line: 20,
+    top: 40,
+    right: 30,
+    bottom: 3 * 20,
+    left: 5 * 20,
+    font_size: 18,
+    rect_size: 22,
+    width: 1024,
+    height: 600,
+  };
+
+  // Only accept time_series with time that is a date
+  time_series = _.filter(time_series, (ts) => _.isDate(ts.time));
+
+  if (time_series.length === 0) {
+    return;
+  }
+
+  var do_barchart = options && options["barchart"];
+  var skip_cumulative = (options && options["skip_cumulative"]) || do_barchart;
+
+  var width = options.width - options.left - options.right;
+  var height = options.height - options.top - options.bottom;
+  var min_diff;
+
+  if (!bin_by) {
+    bin_by = function (date) {
+      var year = date.getFullYear(),
+        nearest_quarter = new Date(),
+        mid_point = new Date();
+
+      nearest_quarter.setDate(1);
+      nearest_quarter.setFullYear(year);
+      mid_point.setFullYear(year);
+
+      var quarter = Math.floor(date.getMonth() / 3);
+
+      nearest_quarter.setMonth(quarter * 3);
+      nearest_quarter.setHours(0, 0, 0);
+      mid_point.setHours(0, 0, 0);
+
+      nearest_quarter.setFullYear(year);
+      mid_point.setMonth(quarter * 3 + 1);
+      mid_point.setDate(15);
+
+      return ["Q" + (quarter + 1) + " " + year, nearest_quarter, mid_point];
+    };
+
+    min_diff = new Date(2018, 3, 0) - new Date(2018, 0, 0);
+  }
+
+  var x_tick_format = function (d) {
+    var year = d.getFullYear();
+    var quarter = Math.floor(d.getMonth() / 3) + 1;
+
+    return String(year) + "-Q" + quarter;
+  };
+
+  if (options && options["x-tick-format"]) {
+    x_tick_format = options["x-tick-format"];
+  }
+
+  /** plot_data is an array with entries like
+        {
+            "time": DATE,
+            "sex_trans":"IDU-Male"
+        }
+
+        "time" is required, everything else are optional attributes
+
+        1. First, we bin everything into ranges (like years or quarters, this is returned by the mapper callback)
+        2. Second, we compute growth dynamics of total counts and individual attributes
+        3. Third, if additional attributes are present, one that's tagged for display is stratified by values and
+           converted into time series
+
+    */
+
+  var x = d3.time.scale().range([0, width]);
+
+  var y = y_scale ? y_scale : d3.scale.linear();
+
+  if (!y_scale) {
+    y.rangeRound([height, 0]);
+  } else {
+    y.range([height, 0]);
+  }
+
+  var xAxis = d3.svg
+    .axis()
+    .scale(x)
+    .orient("bottom")
+    .ticks(d3.time.month, 3)
+    .tickFormat(d3.time.format("%m/%Y"));
+
+  if (x_tick_format) {
+    xAxis.tickFormat(x_tick_format);
+  }
+
+  var yAxis = d3.svg
+    .axis()
+    .scale(y)
+    .orient("left")
+    .tickFormat((v) => {
+      if (v << 0 === v) {
+        // an integer
+        return v;
+      }
+      return null;
+    });
+
+  var binned = {};
+  var values_by_attribute = {};
+  var total_id = "total";
+  var total_color = "#555555";
+  var prefix = options && options["prefix"] ? options["prefix"] : "";
+  var max_bin = 0;
+
+  _.each(time_series, (point, index) => {
+    var bin_tag = bin_by(point["time"]);
+
+    if (!(bin_tag[0] in binned)) {
+      binned[bin_tag[0]] = { time: bin_tag[1], x: bin_tag[2] };
+      binned[bin_tag[0]][total_id] = 0;
+      _.each(point, (v, k) => {
+        if (k !== "time") {
+          binned[bin_tag[0]][k] = {};
+        }
+      });
+    }
+
+    binned[bin_tag[0]][total_id] += 1;
+    max_bin = Math.max(max_bin, binned[bin_tag[0]][total_id]);
+
+    var y = {};
+    y[total_id] = index + 1;
+    _.each(point, (v, k) => {
+      if (k !== "time") {
+        binned[bin_tag[0]][k][v] = binned[bin_tag[0]][k][v]
+          ? binned[bin_tag[0]][k][v] + 1
+          : 1;
+        if (!(k in values_by_attribute)) {
+          values_by_attribute[k] = {};
+        }
+        if (v in values_by_attribute[k]) {
+          values_by_attribute[k][v]++;
+        } else {
+          values_by_attribute[k][v] = 1;
+        }
+        max_bin = Math.max(max_bin, binned[bin_tag[0]][k][v]);
+        y[k] = _.clone(values_by_attribute[k]);
+      }
+    });
+
+    point["y"] = y;
+    point["_bin"] = bin_tag[1];
+  });
+
+  var binned_array = [];
+  _.each(binned, (v, k) => {
+    v["id"] = k;
+    binned_array.push(v);
+  });
+
+  binned_array.sort((a, b) => b["time"] - a["time"]);
+
+  if (do_barchart) {
+    if (_.isUndefined(min_diff)) {
+      _.each(binned_array, (d, i) => {
+        if (i > 0) {
+          min_diff = Math.min(
+            min_diff,
+            -(d["time"] - binned_array[i - 1]["time"])
+          );
+        }
+      });
+    }
+    min_diff *= 0.8; // convert to seconds and shrink a bit
+  }
+
+  var min_x = d3.min(time_series, (d) =>
+    d["time"] < d["_bin"] ? d["time"] : d["_bin"]
+  );
+  var max_x = d3.max(time_series, (d) =>
+    d["time"] > d["_bin"] ? d["time"] : d["_bin"]
+  );
+
+  if (do_barchart) {
+    var max_x2 = new Date();
+    max_x2.setTime(max_x.getTime() + min_diff);
+    max_x = max_x2;
+    max_x2 = new Date();
+    max_x2.setTime(min_x.getTime() - min_diff);
+    min_x = max_x2;
+  }
+
+  let quarter_span = Math.floor((max_x - min_x) / 3600 / 24 / 1000 / 30);
+  if (quarter_span > 8) {
+    xAxis.ticks(d3.time.month, 3 * Math.ceil(quarter_span / 8));
+  }
+
+  x.domain([min_x, max_x]).clamp(true);
+  y.domain([
+    0.0,
+    Math.round(skip_cumulative ? max_bin + 1 : time_series.length * 1.2),
+  ]).clamp(true);
+
+  /* step-plot generator*/
+
+  /*var svg = container.append("svg")//.style("display", "table-cell")
+        .attr("width", width + options.left + options.right)
+        .attr("height", height + options.top + options.bottom);*/
+
+  container.selectAll("*").remove(); // clean up previous plots
+
+  var svg = container
+    .append("g")
+    .attr("transform", "translate(" + options.left + "," + options.top + ")");
+
+  var legend_area = container
+    .append("g")
+    .attr(
+      "transform",
+      "translate(" +
+        (options.left + options.font_size * 2.5) +
+        "," +
+        (options.top + options.font_size) +
+        ")"
+    );
+
+  /* set the domain for the codons */
+
+  var y_key = _.keys(values_by_attribute)[0];
+
+  var color_scale =
+    "colorizer" in options &&
+    options["colorizer"] &&
+    y_key in options["colorizer"]
+      ? options["colorizer"][y_key]
+      : d3.scale.category10();
+
+  color_scale = _.wrap(color_scale, (func, arg) => {
+    if (arg === total_id) return total_color;
+    return func(arg);
+  });
+
+  var plot_types = _.keys(values_by_attribute[y_key]);
+
+  if (do_barchart) {
+    if (plot_types.length === 0) {
+      plot_types.push(total_id);
+    }
+  } else {
+    plot_types.push(total_id);
+  }
+
+  plot_types.sort();
+
+  if (options && options["drag"]) {
+    var drag = d3.behavior.drag();
+    drag.on("drag", function () {
+      options["drag"].x += d3.event.dx;
+      options["drag"].y += d3.event.dy;
+      d3.select(this).attr(
+        "transform",
+        "translate(" + options["drag"].x + "," + options["drag"].y + ")"
+      );
+    });
+    container.call(drag);
+  }
+
+  function opacity_toggle(tag, on_off) {
+    if (do_barchart) {
+      d3.selectAll('[data-plotid="' + tag + '"]').style(
+        "stroke-width",
+        on_off ? 4 : 1
+      );
+    } else {
+      d3.selectAll('[data-plotid="' + tag + '"]').style(
+        "fill-opacity",
+        on_off ? 0.5 : 0.1
+      );
+    }
+    d3.selectAll('[data-curveid="' + tag + '"]').style(
+      "stroke-width",
+      on_off ? 3 : 1
+    );
+  }
+
+  if (!do_barchart || plot_types.length > 1 || plot_types[0] !== total_id) {
+    var legend_lines = legend_area.selectAll("g").data(plot_types);
+
+    legend_lines.enter().append("g").attr("class", "annotation-text");
+
+    legend_lines
+      .selectAll("text")
+      .data((d) => [d])
+      .enter()
+      .append("text")
+      .attr(
+        "transform",
+        (d, i, j) =>
+          "translate(" +
+          options.rect_size +
+          "," +
+          (options.rect_size * (plot_types.length - 1 - j) -
+            (options.rect_size - options.font_size)) +
+          ")"
+      )
+      .attr("dx", "0.2em")
+      .style("font-size", options.font_size)
+      .text((d) => d)
+      .on("mouseover", (d) => {
+        opacity_toggle(prefix + d, true);
+      })
+      .on("mouseout", (d) => {
+        opacity_toggle(prefix + d, false);
+      });
+
+    legend_lines
+      .selectAll("rect")
+      .data((d) => [d])
+      .enter()
+      .append("rect")
+      .attr("x", 0)
+      .attr("y", (d, i, j) => options.rect_size * (plot_types.length - 2 - j))
+      .attr("width", options.rect_size)
+      .attr("height", options.rect_size)
+      .attr("class", "area")
+      .style("fill", (d, i, j) => color_scale(d))
+      .on("mouseover", (d) => {
+        opacity_toggle(prefix + d, true);
+      })
+      .on("mouseout", (d) => {
+        opacity_toggle(prefix + d, false);
+      });
+  }
+
+  var last = _.clone(time_series[time_series.length - 1]);
+  last["time"] = x.domain()[1];
+  time_series.push(last);
+
+  _.each(plot_types, (plot_key, idx) => {
+    var plot_color = color_scale(plot_key);
+    var y_accessor = function (d) {
+      //console.log ((plot_key in d['y']) ? d['y'][plot_key] : 0);
+      if (plot_key in d["y"]) {
+        return d["y"][plot_key];
+      }
+      if (y_key in d["y"]) {
+        if (plot_key in d["y"][y_key]) {
+          return d["y"][y_key][plot_key];
+        }
+      }
+      return 0.0;
+    };
+
+    var bin_accessor = function (d) {
+      if (y_key && plot_key in d[y_key]) {
+        return d[y_key][plot_key];
+      } else if (plot_key in d) {
+        return d[plot_key];
+      }
+      return 0.0;
+    };
+
+    if (!skip_cumulative) {
+      var curve = d3.svg
+        .area()
+        .x((d) => x(d["time"]))
+        .y1((d) => y(y_accessor(d)))
+        .y0((d) => y(0))
+        .interpolate("step");
+
+      svg
+        .append("path")
+        .datum(time_series)
+        .classed("trend", true)
+        .style("fill", plot_color)
+        .style("stroke", plot_color)
+        .attr("d", curve)
+        .attr("data-plotid", prefix + plot_key);
+    }
+
+    if (do_barchart) {
+      binned_array.forEach((d) => {
+        var dd = new Date();
+        dd.setTime(d["time"].getTime() - min_diff * 0.5);
+        var dd2 = new Date();
+        dd2.setTime(d["time"].getTime() + min_diff * 0.5);
+        var xc = x(dd);
+        var w = x(dd2) - x(dd);
+        var last_y = "last_y" in d ? d["last_y"] : 0;
+        var new_y = bin_accessor(d);
+        svg
+          .append("rect")
+          .attr("x", xc)
+          .attr("y", y(last_y + new_y))
+          .attr("height", y(0) - y(new_y))
+          .attr("width", w)
+          .attr("data-plotid", prefix + plot_key)
+          .classed("tracer", true)
+          .style("fill", plot_color)
+          .style("stroke", d3.rgb(plot_color).darker(2))
+          .style("fill-opacity", 1)
+          .append("title")
+          .text(
+            plot_key +
+              " " +
+              new_y +
+              " cases in " +
+              (x_tick_format ? x_tick_format(d["time"]) : d["time"])
+          );
+
+        d["last_y"] = (d["last_y"] ? d["last_y"] : 0) + new_y;
+      });
+    } else {
+      binned_array.forEach((d) => {
+        svg
+          .append("circle")
+          .attr("cx", x(d["time"]))
+          .attr("cy", y(bin_accessor(d)))
+          .attr("r", "5")
+          .classed("node", true)
+          .style("fill", plot_color)
+          .style("stroke", plot_color)
+          .attr("title", plot_key + " : " + bin_accessor(d));
+      });
+
+      var curve_year = d3.svg
+        .line()
+        .x((d) => x(d["time"]))
+        .y((d) => y(bin_accessor(d)))
+        .interpolate("cardinal");
+
+      svg
+        .append("path")
+        .datum(binned_array)
+        .classed("tracer", true)
+        .style("stroke", plot_color)
+        .attr("d", curve_year)
+        .attr("data-curveid", prefix + plot_key);
+    }
+  });
+
+  /* x-axis */
+  var x_axis = svg
+    .append("g")
+    .attr("class", "x axis")
+    .attr("transform", "translate(0," + height + ")")
+    .style("font-size", options.font_size)
+    .call(xAxis);
+
+  x_axis
+    .selectAll("text")
+    .attr("transform", "rotate(-45)")
+    .attr("dy", "0.9em")
+    .attr("dx", "-1.75em");
+
+  x_axis
+    .append("text")
+    .attr("x", width / 2)
+    .attr("dy", "3.5em")
+    .style("text-anchor", "middle")
+    .style("font-size", options.font_size * 1.5)
+    .text(x_title);
+
+  /* y-axis*/
+  svg
+    .append("g")
+    .attr("class", "y axis")
+    .style("font-size", options.font_size)
+    .call(yAxis)
+    .append("text")
+    .style("font-size", options.font_size * 1.5)
+    .attr("transform", "rotate(-90)")
+    .attr("y", 6)
+    .attr("dy", "-2em")
+    //.attr("dx", "-1em")
+    .style("text-anchor", "end")
+    .text(y_title); // beta - alpha
+}
+
+/** Retrieves a CSS selector for UI elements based on their `data-hivtrace-ui-role` attribute.
+
+ * @param {string} role - The value of the `data-hivtrace-ui-role` attribute.
+
+ * @returns {string} A CSS selector string targeting elements with the specified role.
+*/
+
+function get_ui_element_selector_by_role(role) {
+  return ` [data-hivtrace-ui-role='${role}']`;
+}
+
+module.exports = {
+  edge_typer,
+  coi_timeseries: hivtrace_coi_timeseries,
+  compute_node_degrees: hivtrace_compute_node_degrees,
+  export_table_to_text: hiv_trace_export_table_to_text,
+  symbol: hivtrace_generate_svg_symbol,
+  cluster_dynamics: hivtrace_plot_cluster_dynamics,
+  hivtrace_cluster_depthwise_traversal,
+  random_id,
+  get_ui_element_selector_by_role,
+};
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/network.js.html b/docs/network.js.html new file mode 100644 index 0000000..2685d9d --- /dev/null +++ b/docs/network.js.html @@ -0,0 +1,333 @@ + + + + + JSDoc: Source: network.js + + + + + + + + + + +
+ +

Source: network.js

+ + + + + + +
+
+
/**
+    Functions that help manipulate network JSON and perform 
+    other utility operations
+*/
+
+var d3 = require("d3"),
+  _ = require("underscore"),
+  clustersOfInterest = require("./clustersOfInterest.js"),
+  kGlobals = require("./globals.js");
+
+/**
+    unpack_compact_json:
+    If the input network JSON is in compact form, i.e. instead of storing 
+        key : value
+    it stores
+        key : integer index of value
+        unique_values: list of values
+    convert it to 
+        key: value 
+        
+    The operation is performed in place on the `json` argument
+*/
+
+function unpack_compact_json(json) {
+  _.each(["Nodes", "Edges"], (key) => {
+    var fields = _.keys(json[key]);
+    var expanded = [];
+    _.each(fields, (f, idx) => {
+      var field_values = json[key][f];
+      if (!_.isArray(field_values) && "values" in field_values) {
+        var expanded_values = [];
+        _.each(field_values["values"], (v) => {
+          expanded_values.push(field_values["keys"][v]);
+        });
+        field_values = expanded_values;
+      }
+      _.each(field_values, (fv, j) => {
+        if (idx === 0) {
+          expanded.push({});
+        }
+        expanded[j][f] = fv;
+      });
+    });
+    json[key] = expanded;
+  });
+}
+
+/**
+    normalize_node_attributes
+    
+    Iterate over node attributes, lower case all the keys for mapping.
+    If attributes are found that are not in the data dictionary, attempt to map them using 
+    "labels". 
+*/
+function normalize_node_attributes(json) {
+  const label_key_map = _.object(
+    _.map(json[kGlobals.network.GraphAttrbuteID], (d, k) => [d.label, k])
+  );
+
+  _.each(json.Nodes, (n) => {
+    if (kGlobals.network.NodeAttributeID in n) {
+      let new_attrs = {};
+      if (n[kGlobals.network.NodeAttributeID] !== null) {
+        new_attrs = Object.fromEntries(
+          Object.entries(n[kGlobals.network.NodeAttributeID]).map(([k, v]) => [
+            k.toLowerCase(),
+            v,
+          ])
+        );
+      }
+
+      // Map attributes from patient_schema labels to keys, if necessary
+      const unrecognizedKeys = _.difference(
+        _.keys(new_attrs),
+        _.keys(json[kGlobals.network.GraphAttrbuteID])
+      );
+
+      if (unrecognizedKeys.length) {
+        _.each(unrecognizedKeys, (k) => {
+          if (_.contains(_.keys(label_key_map), k)) {
+            new_attrs[label_key_map[k]] = new_attrs[k];
+            delete new_attrs[k];
+          }
+        });
+      }
+
+      n[kGlobals.network.NodeAttributeID] = new_attrs;
+    }
+  });
+}
+/**
+    ensure_node_attributes_exist
+    
+    Iterate over nodes in the network. If a node does not have an array of attributes or 
+    data dictionary records, create an empty one. This makes error checking less complex downstream.
+*/
+
+function ensure_node_attributes_exist(json) {
+  const validate_these_keys = new Set([
+    "attributes",
+    kGlobals.network.NodeAttributeID,
+  ]);
+  json.Nodes.forEach((n) => {
+    for (const i of validate_these_keys) {
+      if (!n[i]) {
+        n[i] = [];
+      }
+    }
+  });
+}
+
+function check_network_option(options, key, if_absent, if_present) {
+  /**
+    check_network_option
+    
+    Given a dictionary option list (can be null) and a key
+    checks to see if the key is present
+    
+        if the key is absent or "options" is null, the return value will be "if_absent" (null by default)
+        if the key is present, and `if_present` is set, will return the if_present value, otherwise will return options[key]
+*/
+
+  if (options) {
+    if (key in options) {
+      return if_present === undefined ? options[key] : if_present;
+    }
+  }
+  return if_absent;
+}
+
+function center_cluster_handler(self, d) {
+  d.x = self.width / 2;
+  d.y = self.height / 2;
+  self.update(false, 0.4);
+}
+
+/**
+    handle_cluster_click
+    
+    Handle contextual menus for clusters and cluster drag 
+    
+    @param self: network object
+    @param cluster [optional]: the cluster object to act on
+    @param release [optional]: the cluster object to release the "fixed" flag from
+*/
+
+function handle_cluster_click(self, cluster, release) {
+  var container = d3.select(self.container);
+  var id = "d3_context_menu_id";
+  var menu_object = container.select("#" + id);
+
+  if (menu_object.empty()) {
+    menu_object = container
+      .append("ul")
+      .attr("id", id)
+      .attr("class", "dropdown-menu")
+      .attr("role", "menu");
+  }
+
+  menu_object.selectAll("li").remove();
+
+  var already_fixed = cluster && cluster.fixed;
+
+  if (cluster) {
+    menu_object
+      .append("li")
+      .append("a")
+      .attr("tabindex", "-1")
+      .text("Expand cluster")
+      .on("click", (d) => {
+        cluster.fixed = 0;
+        self.expand_cluster_handler(cluster, true);
+        menu_object.style("display", "none");
+      });
+
+    menu_object
+      .append("li")
+      .append("a")
+      .attr("tabindex", "-1")
+      .text("Center on screen")
+      .on("click", (d) => {
+        cluster.fixed = 0;
+        center_cluster_handler(self, cluster);
+        menu_object.style("display", "none");
+      });
+
+    menu_object
+      .append("li")
+      .append("a")
+      .attr("tabindex", "-1")
+      .text((d) => {
+        if (cluster.fixed) return "Allow cluster to float";
+        return "Hold cluster at current position";
+      })
+      .on("click", (d) => {
+        cluster.fixed = !cluster.fixed;
+        menu_object.style("display", "none");
+      });
+
+    if (self.isPrimaryGraph) {
+      menu_object
+        .append("li")
+        .append("a")
+        .attr("tabindex", "-1")
+        .text((d) => "Show this cluster in separate tab")
+        .on("click", (d) => {
+          self.open_exclusive_tab_view(
+            cluster.cluster_id,
+            null,
+            null,
+            self._distance_gate_options()
+          );
+          menu_object.style("display", "none");
+        });
+    }
+
+    if (clustersOfInterest.get_editor()) {
+      menu_object
+        .append("li")
+        .append("a")
+        .attr("tabindex", "-1")
+        .text((d) => "Add this cluster to the cluster of interest")
+        .on("click", (d) => {
+          clustersOfInterest
+            .get_editor()
+            .append_nodes(_.map(cluster.children, (c) => c.id));
+        });
+    }
+
+    // Only show the "Show on map" option for clusters with valid country info (for now just 2 letter codes) for each node.
+    const show_on_map_enabled = _.every(
+      cluster.children,
+      (node) => self._get_node_country(node).length === 2
+    );
+
+    if (show_on_map_enabled) {
+      menu_object
+        .append("li")
+        .append("a")
+        .attr("tabindex", "-1")
+        .text("Show on map")
+        .on("click", (d) => {
+          //console.log(cluster)
+          self.open_exclusive_tab_view(
+            cluster.cluster_id,
+            null,
+            (cluster_id) => "Map of cluster: " + cluster_id,
+            { showing_on_map: true }
+          );
+        });
+    }
+
+    //cluster.fixed = 1;
+
+    menu_object
+      .style("position", "absolute")
+      .style("left", String(d3.event.offsetX) + "px")
+      .style("top", String(d3.event.offsetY) + "px")
+      .style("display", "block");
+  } else {
+    if (release) {
+      release.fixed = 0;
+    }
+    menu_object.style("display", "none");
+  }
+
+  container.on(
+    "click",
+    (d) => {
+      handle_cluster_click(self, null, already_fixed ? null : cluster);
+    },
+    true
+  );
+}
+
+module.exports = {
+  check_network_option,
+  ensure_node_attributes_exist,
+  normalize_node_attributes,
+  unpack_compact_json,
+  handle_cluster_click,
+};
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/quicksearch.html b/docs/quicksearch.html deleted file mode 100644 index ac778a4..0000000 --- a/docs/quicksearch.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - diff --git a/docs/scatterplot.js.html b/docs/scatterplot.js.html new file mode 100644 index 0000000..de4947b --- /dev/null +++ b/docs/scatterplot.js.html @@ -0,0 +1,168 @@ + + + + + JSDoc: Source: scatterplot.js + + + + + + + + + + +
+ +

Source: scatterplot.js

+ + + + + + +
+
+
var d3 = require("d3");
+
+/**
+ * Renders a scatter plot using D3.js.
+
+ * @param {Object[]} points - An array of data points, each with x and y coordinates (and optionally a title).
+ * @param {number} w - The width of the plot area.
+ * @param {number} h - The height of the plot area.
+ * @param {string} id - The ID of the HTML element where the plot will be rendered.
+ * @param {Object} labels - An object containing labels for the x and y axes.
+ * @param {boolean} [dates=false] - A flag indicating whether the x-axis should represent dates.
+
+ * @returns {void}
+ */
+
+function hivtrace_render_scatterplot(points, w, h, id, labels, dates) {
+  var _defaultFloatFormat = d3.format(",.2r");
+  var _defaultDateViewFormatShort = d3.time.format("%B %Y");
+
+  var margin = {
+      top: 10,
+      right: 10,
+      bottom: 100,
+      left: 100,
+    },
+    width = w - margin.left - margin.right,
+    height = h - margin.top - margin.bottom;
+
+  var x = (dates ? d3.time.scale() : d3.scale.linear())
+    .domain(d3.extent(points, (p) => p.x))
+    .range([0, width]);
+
+  var y = (dates ? d3.time.scale() : d3.scale.linear())
+    .domain(d3.extent(points, (p) => p.y))
+    .range([height, 0]);
+
+  var xAxis = d3.svg
+    .axis()
+    .scale(x)
+    .orient("bottom")
+    .tickFormat(dates ? _defaultDateViewFormatShort : _defaultFloatFormat);
+
+  var yAxis = d3.svg
+    .axis()
+    .scale(y)
+    .orient("left")
+    .tickFormat(dates ? _defaultDateViewFormatShort : _defaultFloatFormat);
+
+  var histogram_svg = d3.select(id).selectAll("svg");
+
+  if (!histogram_svg.empty()) {
+    histogram_svg.remove();
+  }
+
+  histogram_svg = d3
+    .select(id)
+    .append("svg")
+    .attr("width", w)
+    .attr("height", h)
+    .append("g")
+    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+  points = histogram_svg.selectAll("circle").data(points);
+  points.enter().append("circle");
+
+  points
+    .attr("cx", (d) => x(d.x))
+    .attr("cy", (d) => y(d.y))
+    .attr("r", 3)
+    .classed("node scatter", true);
+
+  points.each(function (d) {
+    if ("title" in d) {
+      d3.select(this).append("title").text(d.title);
+    }
+  });
+
+  var x_axis = histogram_svg
+    .append("g")
+    .attr("class", "x axis")
+    .attr("transform", "translate(0," + height + ")")
+    .call(xAxis);
+
+  x_axis
+    .selectAll("text")
+    .attr("transform", "rotate(-45)")
+    .attr("dx", "-.5em")
+    .attr("dy", ".25em")
+    .style("text-anchor", "end");
+  x_axis
+    .append("text")
+    .text(labels.x)
+    .attr("transform", "translate(" + width + ",0)")
+    .attr("dy", "-1em")
+    .attr("text-anchor", "end");
+
+  var y_axis = histogram_svg
+    .append("g")
+    .attr("class", "y axis")
+    .attr("transform", "translate(0," + 0 + ")")
+    .call(yAxis);
+
+  y_axis
+    .selectAll("text")
+    .attr("transform", "rotate(-45)")
+    .attr("dx", "-.5em")
+    .attr("dy", ".25em")
+    .style("text-anchor", "end");
+  y_axis
+    .append("text")
+    .text(labels.y)
+    .attr("transform", "rotate(-90)")
+    .attr("dy", "1em")
+    .attr("text-anchor", "end");
+}
+
+module.exports.scatterPlot = hivtrace_render_scatterplot;
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/scripts/docstrap.lib.js b/docs/scripts/docstrap.lib.js deleted file mode 100644 index 8d86c43..0000000 --- a/docs/scripts/docstrap.lib.js +++ /dev/null @@ -1,22646 +0,0 @@ -if ( - (!(function (a, b) { - "object" == typeof module && "object" == typeof module.exports - ? (module.exports = a.document - ? b(a, !0) - : function (a) { - if (!a.document) - throw new Error("jQuery requires a window with a document"); - return b(a); - }) - : b(a); - })("undefined" != typeof window ? window : this, function (a, b) { - function c(a) { - var b = "length" in a && a.length, - c = _.type(a); - return ( - "function" !== c && - !_.isWindow(a) && - (!(1 !== a.nodeType || !b) || - "array" === c || - 0 === b || - ("number" == typeof b && b > 0 && b - 1 in a)) - ); - } - function d(a, b, c) { - if (_.isFunction(b)) - return _.grep(a, function (a, d) { - return !!b.call(a, d, a) !== c; - }); - if (b.nodeType) - return _.grep(a, function (a) { - return (a === b) !== c; - }); - if ("string" == typeof b) { - if (ha.test(b)) return _.filter(b, a, c); - b = _.filter(b, a); - } - return _.grep(a, function (a) { - return U.call(b, a) >= 0 !== c; - }); - } - function e(a, b) { - for (; (a = a[b]) && 1 !== a.nodeType; ); - return a; - } - function f(a) { - var b = (oa[a] = {}); - return ( - _.each(a.match(na) || [], function (a, c) { - b[c] = !0; - }), - b - ); - } - function g() { - Z.removeEventListener("DOMContentLoaded", g, !1), - a.removeEventListener("load", g, !1), - _.ready(); - } - function h() { - Object.defineProperty((this.cache = {}), 0, { - get: function () { - return {}; - }, - }), - (this.expando = _.expando + h.uid++); - } - function i(a, b, c) { - var d; - if (void 0 === c && 1 === a.nodeType) - if ( - ((d = "data-" + b.replace(ua, "-$1").toLowerCase()), - (c = a.getAttribute(d)), - "string" == typeof c) - ) { - try { - c = - "true" === c || - ("false" !== c && - ("null" === c - ? null - : +c + "" === c - ? +c - : ta.test(c) - ? _.parseJSON(c) - : c)); - } catch (a) {} - sa.set(a, b, c); - } else c = void 0; - return c; - } - function j() { - return !0; - } - function k() { - return !1; - } - function l() { - try { - return Z.activeElement; - } catch (a) {} - } - function m(a, b) { - return _.nodeName(a, "table") && - _.nodeName(11 !== b.nodeType ? b : b.firstChild, "tr") - ? a.getElementsByTagName("tbody")[0] || - a.appendChild(a.ownerDocument.createElement("tbody")) - : a; - } - function n(a) { - return (a.type = (null !== a.getAttribute("type")) + "/" + a.type), a; - } - function o(a) { - var b = Ka.exec(a.type); - return b ? (a.type = b[1]) : a.removeAttribute("type"), a; - } - function p(a, b) { - for (var c = 0, d = a.length; d > c; c++) - ra.set(a[c], "globalEval", !b || ra.get(b[c], "globalEval")); - } - function q(a, b) { - var c, d, e, f, g, h, i, j; - if (1 === b.nodeType) { - if ( - ra.hasData(a) && - ((f = ra.access(a)), (g = ra.set(b, f)), (j = f.events)) - ) { - delete g.handle, (g.events = {}); - for (e in j) - for (c = 0, d = j[e].length; d > c; c++) _.event.add(b, e, j[e][c]); - } - sa.hasData(a) && - ((h = sa.access(a)), (i = _.extend({}, h)), sa.set(b, i)); - } - } - function r(a, b) { - var c = a.getElementsByTagName - ? a.getElementsByTagName(b || "*") - : a.querySelectorAll - ? a.querySelectorAll(b || "*") - : []; - return void 0 === b || (b && _.nodeName(a, b)) ? _.merge([a], c) : c; - } - function s(a, b) { - var c = b.nodeName.toLowerCase(); - "input" === c && ya.test(a.type) - ? (b.checked = a.checked) - : ("input" === c || "textarea" === c) && - (b.defaultValue = a.defaultValue); - } - function t(b, c) { - var d, - e = _(c.createElement(b)).appendTo(c.body), - f = - a.getDefaultComputedStyle && (d = a.getDefaultComputedStyle(e[0])) - ? d.display - : _.css(e[0], "display"); - return e.detach(), f; - } - function u(a) { - var b = Z, - c = Oa[a]; - return ( - c || - ((c = t(a, b)), - ("none" !== c && c) || - ((Na = ( - Na || _("