diff --git a/html/network.html b/html/network.html index 0734604..e7ca84b 100644 --- a/html/network.html +++ b/html/network.html @@ -55,9 +55,7 @@ - + @@ -614,19 +612,19 @@ d3.select(d3.select(this).attr("href")).style("display", null); }); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#cluster-table-export", cluster_table ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#subcluster-table-export", subcluster_table ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#node-table-export", node_table ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#priority-table-export", priority_set_table ); diff --git a/html/plain.html b/html/plain.html index b0cdf56..ca0cefb 100644 --- a/html/plain.html +++ b/html/plain.html @@ -51,9 +51,7 @@ - + @@ -571,11 +569,11 @@ d3.select(d3.select(this).attr("href")).style("display", null); }); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#cluster-table-export", cluster_table ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#node-table-export", node_table ); diff --git a/html/priority-sets-args.html b/html/priority-sets-args.html index 1a4b311..7be2b5a 100644 --- a/html/priority-sets-args.html +++ b/html/priority-sets-args.html @@ -55,9 +55,7 @@ - + @@ -673,17 +671,17 @@ d3.select(d3.select(this).attr("href")).style("display", null); }); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#cluster-table-export", cluster_table, true ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#subcluster-table-export", subcluster_table, true ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#node-table-export", node_table, true diff --git a/html/social.html b/html/social.html index 9ba6d09..696f1b7 100644 --- a/html/social.html +++ b/html/social.html @@ -60,9 +60,7 @@ - + @@ -591,15 +589,15 @@ d3.select(d3.select(this).attr("href")).style("display", null); }); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#cluster-table-export", cluster_table ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#subcluster-table-export", subcluster_table ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#node-table-export", node_table ); diff --git a/index.html b/index.html index 84c22b0..4c87938 100644 --- a/index.html +++ b/index.html @@ -52,9 +52,7 @@ - + @@ -593,15 +591,15 @@ d3.select(d3.select(this).attr("href")).style("display", null); }); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#cluster-table-export", cluster_table ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#subcluster-table-export", subcluster_table ); - hivtrace.misc.export_table_to_text( + hivtrace.helpers.render_button_export_table_to_text( "#node-table-export", node_table ); diff --git a/refactor/README.md b/refactor/README.md new file mode 100644 index 0000000..d71d4a4 --- /dev/null +++ b/refactor/README.md @@ -0,0 +1,7 @@ +Some scripts to assist / automate the refactoring process. + +`function-usages.py`: +Provide a regex expression (optional) and files to search within. The script returns all instances of the regex expression that are used only in one file (and is useful since a function that is only used in one file can just be moved to that file). + +For example, finding `clusternetwork.js` `self` functions that can be refactored from `clusterOI.js`, run: +`python3 refactor/function-usages.py src/clusternetwork.js src/clustersOI/clusterOI.js` \ No newline at end of file diff --git a/refactor/function-usages.py b/refactor/function-usages.py new file mode 100755 index 0000000..c086131 --- /dev/null +++ b/refactor/function-usages.py @@ -0,0 +1,59 @@ +import sys +import os +import re +import argparse + +# Define the regex pattern +regex = re.escape("self.") + "[a-zA-Z0-9_]+" + re.escape("(") + +parser = argparse.ArgumentParser(description='Find function usages in a list of files') +# optional regex pattern +parser.add_argument('--pattern', type=str, help='a regex pattern to search for. expects last character to be an open parenthesis') +parser.add_argument('--no-paranthesis', action='store_true', help='if set, will not expect the last character of the pattern to be an open paranthesis') +parser.add_argument('files', metavar='files', type=str, nargs='+', + help='a list of files to search for function usages') + +# Parse the arguments +args = parser.parse_args() +if args.pattern: + regex = args.pattern + +# records where a function is used, and which functions are used in a file +function_to_files = {} +files_to_functions = {} + +# Iterate over the provided file list +for file in args.files: + # Check if the file exists + if os.path.isfile(file): + # Open the file and search for the regex pattern + with open(file, 'r') as f: + content = f.read() + functions = re.findall(regex, content) + if not args.no_paranthesis: + functions = [function[:-1] for function in functions] + + for function in functions: + if function not in function_to_files: + function_to_files[function] = {} + if file not in function_to_files[function]: + function_to_files[function][file] = 1 + else: + function_to_files[function][file] += 1 + + if file not in files_to_functions: + files_to_functions[file] = {} + if function not in files_to_functions[file]: + files_to_functions[file][function] = 1 + else: + files_to_functions[file][function] += 1 + else: + print(f"File not found: {file}") + sys.exit(1) + +for file in files_to_functions: + print(f"File {file} exclusively uses: ") + for function in files_to_functions[file]: + files_used_in = function_to_files[function].keys() + if len(files_used_in) == 1: + print(f" {function} ({files_to_functions[file][function]} times)") \ No newline at end of file diff --git a/src/clusternetwork.js b/src/clusternetwork.js index 53d880d..71e72a2 100755 --- a/src/clusternetwork.js +++ b/src/clusternetwork.js @@ -1,26 +1,23 @@ 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 utils from "./utils.js"; +import * as helpers from "./helpers.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 clustersOfInterest from "./clustersOI/clusterOI.js"; +import { hivtrace_cluster_depthwise_traversal } from "./helpers.js"; +import * as svgPlots from "./svgPlots.js"; -const _networkSubclusterSeparator = "."; -const _networkGraphAttrbuteID = "patient_attribute_schema"; -const _networkNodeAttributeID = "patient_attributes"; +export const _networkSubclusterSeparator = "."; +export const _networkGraphAttrbuteID = "patient_attribute_schema"; +export const _networkNodeAttributeID = "patient_attributes"; export const _networkMissing = __("general")["missing"]; -const _networkReducedValue = "Different (other) value"; -const _networkMissingOpacity = "0.1"; -const _networkMissingColor = "#999"; -const _networkContinuousColorStops = 9; +export const _networkReducedValue = "Different (other) value"; +export const _networkMissingOpacity = "0.1"; +export const _networkMissingColor = "#999"; +export const _networkContinuousColorStops = 9; export const _networkWarnExecutiveMode = "This feature is not available in the executive mode."; @@ -35,7 +32,7 @@ const _networkShapeOrdering = [ "pentagon", ]; -const _defaultFloatFormat = d3.format(",.2r"); +export const _defaultFloatFormat = d3.format(",.2r"); export const _defaultPercentFormat = d3.format(",.3p"); export const _defaultPercentFormatShort = d3.format(".2p"); export const _defaultDateFormats = [d3.time.format.iso]; @@ -44,9 +41,7 @@ export const _defaultDateViewFormatMMDDYYY = d3.time.format("%m%d%Y"); export const _defaultDateViewFormat = d3.time.format("%b %d, %Y"); const _defaultDateViewFormatShort = d3.time.format("%B %Y"); export const _defaultDateViewFormatSlider = d3.time.format("%Y-%m-%d"); -const _networkDotFormatPadder = d3.format("08d"); -const _defaultDateViewFormatExport = d3.time.format("%m/%d/%Y"); -const _defaultDateViewFormatClusterCreate = d3.time.format("%Y%m"); +export const _defaultDateViewFormatClusterCreate = d3.time.format("%Y%m"); const _networkCategoricalBase = [ "#a6cee3", @@ -259,13 +254,13 @@ _cdcConciseTrackingOptions[_cdcTrackingOptions[2]] = "3 years, 1.5% distance"; _cdcConciseTrackingOptions[_cdcTrackingOptions[3]] = "1.5% distance"; _cdcConciseTrackingOptions[_cdcTrackingOptions[4]] = "None"; -const _cdcTrackingOptionsFilter = {}; +export const _cdcTrackingOptionsFilter = {}; _cdcTrackingOptionsFilter[_cdcTrackingOptions[0]] = (e, d) => e.length < 0.005; _cdcTrackingOptionsFilter[_cdcTrackingOptions[1]] = (e, d) => e.length < 0.005; _cdcTrackingOptionsFilter[_cdcTrackingOptions[2]] = (e, d) => e.length < 0.015; _cdcTrackingOptionsFilter[_cdcTrackingOptions[3]] = (e, d) => e.length < 0.015; -const _cdcTrackingOptionsCutoff = {}; +export const _cdcTrackingOptionsCutoff = {}; _cdcTrackingOptionsCutoff[_cdcTrackingOptions[0]] = 36; _cdcTrackingOptionsCutoff[_cdcTrackingOptions[1]] = 100000; _cdcTrackingOptionsCutoff[_cdcTrackingOptions[2]] = 36; @@ -277,7 +272,7 @@ export const _cdcTrackingNone = _cdcTrackingOptions[4]; export const _cdcCreatedBySystem = "System"; export const _cdcCreatedByManual = "Manual"; -const _cdcPrioritySetKindAutoExpand = { +export const _cdcPrioritySetKindAutoExpand = { "01 state/local molecular cluster analysis": true, }; @@ -286,7 +281,7 @@ export const _cdcPrioritySetNodeKind = [ "02 through investigation", ]; -const _cdcPOImember = "Ever in national priority clusterOI?"; +export const _cdcPOImember = "Ever in national priority clusterOI?"; const _cdcJurisdictionCodes = { alabama: "al", @@ -381,19 +376,9 @@ const _cdcJurisdictionLowMorbidity = new Set([ "wyoming", ]); -const _cdcPrioritySetKindAutomaticCreation = _cdcPrioritySetKind[0]; +export const _cdcPrioritySetKindAutomaticCreation = _cdcPrioritySetKind[0]; export const _cdcPrioritySetDefaultNodeKind = _cdcPrioritySetNodeKind[0]; -// Constants for the map. - -var hivtrace_date_or_na_if_missing = (date, formatter) => { - formatter = formatter || _defaultDateViewFormatExport; - if (date) { - return formatter(date); - } - return "N/A"; -}; - const _networkUpperBoundOnDate = new Date().getFullYear(); var hivtrace_cluster_network_graph = function ( @@ -526,7 +511,7 @@ var hivtrace_cluster_network_graph = function ( self.json = json; self.uniqs = helpers.get_unique_count(json.Nodes, new_schema); - self.uniqValues = helpers.getUniqueValues(json.Nodes, new_schema); + self.uniqValues = helpers.get_unique_values(json.Nodes, new_schema); self.schema = json[_networkGraphAttrbuteID]; // set initial color schemes @@ -593,7 +578,7 @@ var hivtrace_cluster_network_graph = function ( self.today = new Date(json.Settings.created); } else { self.today = - options && options["today"] ? options["today"] : timeDateUtil.getCurrentDate(); + options && options["today"] ? options["today"] : helpers.getCurrentDate(); } self.get_reference_date = function () { @@ -777,7 +762,7 @@ var hivtrace_cluster_network_graph = function ( list_element.selectAll("li").remove(); let check_membership = _.filter( - _.map(self.defined_priority_groups, (g) => + _.map(clustersOfInterest.get_pg(), (g) => //console.log(g); [ g.name, @@ -935,7 +920,7 @@ var hivtrace_cluster_network_graph = function ( }, generator: function (node) { return { - callback: function (element, payload) { + callback: function (self, element, payload) { //payload = _.filter (payload, function (d) {return d}); var this_cell = d3.select(element); @@ -1017,1545 +1002,1165 @@ var hivtrace_cluster_network_graph = function ( return undefined; } return ( - (not_nested ? "" : "#" + button_bar_ui) + utils.get_ui_element_selector_by_role(role) + (not_nested ? "" : "#" + button_bar_ui) + helpers.get_ui_element_selector_by_role(role) ); }; //--------------------------------------------------------------------------------------------------- - // BEGIN: NODE SET GROUPS + // BEGIN: NODE SET EDITOR //--------------------------------------------------------------------------------------------------- - self.defined_priority_groups = []; - /** - { - 'name' : 'unique name', - 'nodes' : [ - { - 'node_id' : text, - 'added' : date, - 'kind' : text - }], - 'created' : date, - 'description' : 'text', - 'modified' : date, - 'kind' : 'text' - } - */ - - self.priority_groups_pending = function () { - return _.filter(self.defined_priority_groups, (pg) => pg.pending).length; - }; - self.priority_groups_expanded = function () { - return _.filter(self.defined_priority_groups, (pg) => pg.expanded).length; - }; - self.priority_groups_automatic = function () { - return _.filter( - self.defined_priority_groups, - (pg) => pg.createdBy === _cdcCreatedBySystem - ).length; - }; + clustersOfInterest.init(self); - self._generate_auto_id = function (subcluster_id) { - const id = - self.CDC_data["jurisdiction_code"] + - "_" + - _defaultDateViewFormatClusterCreate(self.CDC_data["timestamp"]) + - "_" + - subcluster_id; - let suffix = ""; - let k = 1; - let found = self.auto_create_priority_sets.find((d) => d.name === id + suffix) - || self.defined_priority_groups.find((d) => d.name === id + suffix); - while (found !== undefined) { - suffix = "_" + k; - k++; - found = self.auto_create_priority_sets.find((d) => d.name === id + suffix) - || self.defined_priority_groups.find((d) => d.name === id + suffix); - } - return id + suffix; - } + //--------------------------------------------------------------------------------------------------- + // END: NODE SET EDITOR + //--------------------------------------------------------------------------------------------------- - self.load_priority_sets = function (url, is_writeable) { - d3.json(url, (error, results) => { - if (error) { - throw Error("Failed loading cluster of interest file " + error.responseURL); - } else { - let latest_date = new Date(); - latest_date.setFullYear(1900); - self.defined_priority_groups = _.clone(results); - _.each(self.defined_priority_groups, (pg) => { - _.each(pg.nodes, (n) => { - try { - n.added = _defaultDateFormats[0].parse(n.added); - if (n.added > latest_date) { - latest_date = n.added; - } - } catch (e) { - // do nothing - } - }); - }); + //--------------------------------------------------------------------------------------------------- - self.priority_set_table_writeable = is_writeable === "writeable"; + self.node_shaper = { + id: null, + shaper: function () { + return "circle"; + }, + }; - self.priority_groups_validate( - self.defined_priority_groups, - self._is_CDC_auto_mode - ); + nodesTab.init(d3.select(nodes_table)); - self.auto_create_priority_sets = []; - // propose some - const today_string = _defaultDateFormats[0](self.today); - const node_id_to_object = {}; + self.filter_edges = true; + self.hide_hxb2 = false; + self.charge_correction = 5; + 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.cluster_table = d3.select(clusters_table); + self.priority_set_table = + self._is_CDC_ && options && options["priority-table"] + ? d3.select(options["priority-table"]) + : null; + self.priority_set_table_write = + self._is_CDC_ && options && options["priority-table-writeback"] + ? options["priority-table-writeback"] + : null; + self.needs_an_update = false; + self.hide_unselected = false; + self.show_percent_in_pairwise_table = false; + self.gradient_id = 0; - _.each(self.json.Nodes, (n, i) => { - node_id_to_object[n.id] = n; - }); + self.priority_set_table_writeable = true; - if (self._is_CDC_auto_mode) { - _.each(self.clusters, (cluster_data, cluster_id) => { - _.each(cluster_data.subclusters, (subcluster_data) => { - _.each(subcluster_data.priority_score, (priority_score, i) => { - if ( - priority_score.length >= - self.CDC_data["autocreate-priority-set-size"] - ) { - // only generate a new set if it doesn't match what is already there - const node_set = {}; - _.each(subcluster_data.recent_nodes[i], (n) => { - node_set[n] = 1; - }); + self._calc_country_nodes = function (calc_options) { + if (calc_options && "country-centers" in calc_options) { + self.mapProjection = d3.geo + .mercator() + .translate([ + self.margin.left + self.width / 2, + self.margin.top + self.height / 2, + ]) + .scale((150 * self.width) / 960); + _.each(self.countryCentersObject, (value) => { + value.countryXY = self.mapProjection([value.longt, value.lat]); + }); + } + }; - const matched_groups = _.filter( - _.filter( - self.defined_priority_groups, - (pg) => - pg.kind in _cdcPrioritySetKindAutoExpand && - pg.createdBy === _cdcCreatedBySystem && - pg.tracking === _cdcTrackingOptionsDefault - ), - (pg) => { - const matched = _.countBy( - _.map(pg.nodes, (pn) => pn.name in node_set) - ); - //if (pg.name === 'FL_201709_141.1') console.log (matched); - return ( - //matched[true] === subcluster_data.recent_nodes[i].length - matched[true] >= 1 - ); - } - ); + if ( + options && + "country-centers" in options && + "country-outlines" in options + ) { + self.countryCentersObject = options["country-centers"]; + self.countryOutlines = options["country-outlines"]; + self._calc_country_nodes(options); + //console.log (self.countryCentersObject); + self.showing_on_map = options.showing_on_map; + //console.log (self.showing_on_map); + } else { + self.countryCentersObject = null; + self.showing_on_map = false; + } - if (matched_groups.length >= 1) { - return; - } + self._additional_node_pop_fields = []; + /** 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 */ - const autoname = self._generate_auto_id(subcluster_data.cluster_id); - self.auto_create_priority_sets.push({ - name: autoname, - description: - "Automatically created cluster of interest " + autoname, - nodes: _.map(subcluster_data.recent_nodes[i], (n) => - self.priority_group_node_record(n, self.today) - ), - created: today_string, - kind: _cdcPrioritySetKindAutomaticCreation, - tracking: _cdcTrackingOptions[0], - createdBy: _cdcCreatedBySystem, - autocreated: true, - autoexpanded: false, - pending: true, - }); - } - }); - }); - }); - } + if (options && "minimum size" in options) { + self.minimum_cluster_size = options["minimum size"]; + } else if (self._is_CDC_) { + self.minimum_cluster_size = 5; + } else { + self.minimum_cluster_size = 5; + } - if (self.auto_create_priority_sets.length) { - // SLKP 20200727 now check to see if any of the priority sets - // need to be auto-generated - //console.log (self.auto_create_priority_sets); - self.defined_priority_groups.push(...self.auto_create_priority_sets); - } - const autocreated = self.defined_priority_groups.filter( - (pg) => pg.autocreated - ).length, - autoexpanded = self.defined_priority_groups.filter( - (pg) => pg.autoexpanded - ).length, - automatic_action_taken = autocreated + autoexpanded > 0, - left_to_review = self.defined_priority_groups.filter( - (pg) => pg.pending - ).length; - - if (automatic_action_taken) { - self.warning_string += - "
Automatically created " + - autocreated + - " and expanded " + - autoexpanded + - " clusters of interest." + - (left_to_review > 0 - ? " Please review clusters in the Clusters of Interest tab.
" - : ""); - self.display_warning(self.warning_string, true); - } + helpers.dateTimeInit(options, self._is_CDC_, helpers._networkCDCDateField); - const tab_pill = self.get_ui_element_selector_by_role( - "priority_set_counts", - true - ); + if (self._is_CDC_) { + self._additional_node_pop_fields.push(helpers._networkCDCDateField); + } - if (!self.priority_set_table_writeable) { - const rationale = - is_writeable === "old" - ? "the network is older than some of the Clusters of Interest" - : "the network was ran in standalone mode so no data is stored"; - self.warning_string += `

READ-ONLY mode for Clusters of Interest is enabled because ${rationale}. None of the changes to clustersOI made during this session will be recorded.

`; - self.display_warning(self.warning_string, true); - if (tab_pill) { - d3.select(tab_pill).text("Read-only"); - } - } else if (tab_pill && left_to_review > 0) { - d3.select(tab_pill).text(left_to_review); - d3.select("#banner_coi_counts").text(left_to_review); - } + if (options && "core-link" in options) { + self.core_link_length = options["core-link"]; + } else { + self.core_link_length = -1; + } - self.priority_groups_validate(self.defined_priority_groups); - _.each(self.auto_create_priority_sets, (pg) => - self.priority_groups_update_node_sets(pg.name, "insert") - ); - const groups_that_expanded = self.defined_priority_groups.filter( - (pg) => pg.expanded - ); - _.each(groups_that_expanded, (pg) => - self.priority_groups_update_node_sets(pg.name, "update") - ); + if (options && "edge-styler" in options) { + self.additional_edge_styler = options["edge-styler"]; + } else { + self.additional_edge_styler = null; + } - clustersOfInterest.draw_priority_set_table(self); - if ( - self.showing_diff && - self.has_network_attribute("subcluster_or_priority_node") - ) { - self.handle_attribute_categorical("subcluster_or_priority_node"); - } - //self.update(); - } - }); + self.filter_by_size = function (cluster, value) { + return cluster.children.length >= self.minimum_cluster_size; }; - self.priority_groups_find_by_name = function (name) { - if (self.defined_priority_groups) { - return _.find(self.defined_priority_groups, (g) => g.name === name); - } - return null; + self.filter_singletons = function (cluster, value) { + return cluster.children.length > 1; }; - self.priority_groups_all_events = function () { - // generate a set of all unique temporal events (when new data were added to ANY PG) - const events = new Set(); - if (self.defined_priority_groups) { - _.each(self.defined_priority_groups, (g) => { - _.each(g.nodes, (n) => { - events.add(_defaultDateViewFormatSlider(n.added)); - }); - }); - } - return events; + self.filter_if_added = function (cluster) { + return self.cluster_attributes[cluster.cluster_id].type !== "existing"; }; - self.priority_group_node_record = function (node_id, date, kind) { - return { - name: node_id, - added: date || self.today, - kind: kind || _cdcPrioritySetDefaultNodeKind, - autoadded: true, - }; + self.filter_time_period = function (cluster) { + return _.some(self.nodes_by_cluster[cluster.cluster_id], (n) => + self.attribute_node_value_by_id(n, helpers.getClusterTimeScale()) >= + self.using_time_filter + ); }; - self.priority_groups_compute_overlap = function (groups) { - /** - compute the overlap between priority sets (PS) - - 1. Populate self.priority_node_overlap dictionary which - stores, for every node present in AT LEAST ONE PS, the set of all - PGs it belongs to, as in "node-id" => set ("PG1", "PG2"...) - - 2. For each PS, create and populate a member field, .overlaps - which is a dictionary that stores - { - sets : #of PS with which it shares nodes - nodes: the # of nodes contained in overlaps - } - - */ - self.priority_node_overlap = {}; - const size_by_pg = {}; - _.each(groups, (pg) => { - size_by_pg[pg.name] = pg.nodes.length; - _.each(pg.nodes, (n) => { - if (!(n.name in self.priority_node_overlap)) { - self.priority_node_overlap[n.name] = new Set(); - } - self.priority_node_overlap[n.name].add(pg.name); - }); - }); + self.cluster_filtering_functions = { + size: self.filter_by_size, + singletons: self.filter_singletons, + }; - _.each(groups, (pg) => { - const overlap = { - sets: new Set(), - nodes: 0, - supersets: [], - duplicates: [], - }; + self.using_time_filter = null; - const by_set_count = {}; - _.each(pg.nodes, (n) => { - if (self.priority_node_overlap[n.name].size > 1) { - overlap.nodes++; - self.priority_node_overlap[n.name].forEach((pgn) => { - if (pgn !== pg.name) { - if (!(pgn in by_set_count)) { - by_set_count[pgn] = []; - } - by_set_count[pgn].push(n.name); - } - overlap.sets.add(pgn); - }); - } - }); + if (self.json.Notes) { + _.each(self.json.Notes, (s) => (self.warning_string += s + "
")); + } - _.each(by_set_count, (nodes, name) => { - if (nodes.length === pg.nodes.length) { - if (size_by_pg[name] === pg.nodes.length) { - overlap.duplicates.push(name); - } else { - overlap.supersets.push(name); - } - } - }); + 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 ( + helpers.getClusterTimeScale() && + "Cluster sizes" in self.json && + self.json["Cluster sizes"].length > 250 + ) { + self.using_time_filter = helpers.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; + } + } - pg.overlap = { - nodes: overlap.nodes, - sets: Math.max(0, overlap.sets.size - 1), - superset: overlap.supersets, - duplicate: overlap.duplicates, - }; - }); + self.cluster_display_filter = function (cluster) { + return _.every(self.cluster_filtering_functions, (filter) => filter(cluster)); }; - self.auto_expand_pg_handler = function (pg, nodeID2idx) { - if (!nodeID2idx) { - const nodeset = {}; - nodeID2idx = {}; - _.each(self.json.Nodes, (n, i) => { - nodeset[n.id] = n; - nodeID2idx[n.id] = i; - }); - } - - const core_node_set = new Set(_.map(pg.nodes, (n) => nodeID2idx[n.name])); - const added_nodes = new Set(); - const filter = _cdcTrackingOptionsFilter[pg.tracking]; + self.initial_packed = + options && options["initial_layout"] === "tiled" ? false : true; - if (filter) { - const time_cutoff = _n_months_ago( - self.get_reference_date(), - _cdcTrackingOptionsCutoff[pg.tracking] - ); - const expansion_test = hivtrace_cluster_depthwise_traversal( - self.json.Nodes, - self.json.Edges, - (e) => { - let pass = filter(e); - if (pass) { - if (!(core_node_set.has(e.source) && core_node_set.has(e.target))) { - pass = - pass && - self._filter_by_date( - time_cutoff, - timeDateUtil._networkCDCDateField, - self.get_reference_date(), - self.json.Nodes[e.source] - ) && - self._filter_by_date( - time_cutoff, - timeDateUtil._networkCDCDateField, - self.get_reference_date(), - self.json.Nodes[e.target] - ); - } - } - return pass; - }, - false, - _.filter( - _.map([...core_node_set], (d) => self.json.Nodes[d]), - (d) => d - ) - ); + self.recent_rapid_definition_simple = function (network, date) { + // date = date || self.get_reference_date(); // not used - _.each(expansion_test, (c) => { - _.each(c, (n) => { - if (!core_node_set.has(nodeID2idx[n.id])) { - added_nodes.add(nodeID2idx[n.id]); - } - }); - }); - } - return added_nodes; - }; + var subcluster_enum_simple = [ + "Not a member of national priority clusterOI", // 1,4,5,6 + "12 months and in national priority clusterOI", // 2 + "36 months and in national priority clusterOI", // 3 + ">36 months and in national priority clusterOI", // 0 + ]; - self.priority_groups_validate = function (groups, auto_extend) { - /** - groups is a list of priority groups + return { + depends: [helpers._networkCDCDateField], + label: "Subcluster or Priority Node", + enum: subcluster_enum_simple, + type: "String", + color_scale: function () { + return d3.scale + .ordinal() + .domain(subcluster_enum_simple.concat([_networkMissing])) + .range( + _.union( + ["#CCCCCC", "red", "blue", "#9A4EAE"], + [_networkMissingColor] + ) + ); + }, - name: unique string - description: string, - nodes: { - { - 'id' : node id, - 'added' : date, - 'kind' : enum (one of _cdcPrioritySetNodeKind) + map: function (node) { + if (node.subcluster_label) { + if (node.nationalCOI) { + return subcluster_enum_simple[node.nationalCOI]; } + } + return subcluster_enum_simple[0]; }, - created: date, - kind: enum (one of _cdcPrioritySetKind), - tracking: enum (one of _cdcTrackingOptions) - createdBy : enum (on of [_cdcCreatedBySystem,_cdcCreatedByManual]) - - - */ + }; + }; - if (_.some(groups, (g) => !g.validated)) { - const priority_subclusters = _.map( - _.filter( - _.flatten( - _.map( - _.flatten( - _.map(self.clusters, (c) => - _.filter( - _.filter(c.subclusters, (sc) => sc.priority_score.length) - ) - ) - ), - (d) => d.priority_score - ), - 1 - ), - (d) => d.length >= self.CDC_data["autocreate-priority-set-size"] - ), - (d) => new Set(d) - ); - - const nodeset = {}; - const nodeID2idx = {}; - _.each(self.json.Nodes, (n, i) => { - nodeset[n.id] = n; - nodeID2idx[n.id] = i; - }); - _.each(groups, (pg) => { - if (!pg.validated) { - pg.node_objects = []; - pg.not_in_network = []; - pg.validated = true; - pg.created = _.isDate(pg.created) - ? pg.created - : _defaultDateFormats[0].parse(pg.created); - if (pg.modified) { - pg.modified = _.isDate(pg.modified) ? pg.modified : _defaultDateFormats[0].parse(pg.modified); - } else { - pg.modified = pg.created; - } - if (!pg.tracking) { - if (pg.kind === _cdcPrioritySetKind[0]) { - pg.tracking = _cdcTrackingOptions[0]; - } else { - pg.tracking = _cdcTrackingOptions[4]; - } - } - if (!pg.createdBy) { - if (pg.kind === _cdcPrioritySetKind[0]) { - pg.createdBy = _cdcCreatedBySystem; - } else { - pg.createdBy = _cdcCreatedByManual; - } - } - - _.each(pg.nodes, (node) => { - const nodeid = node.name; - if (nodeid in nodeset) { - pg.node_objects.push(nodeset[nodeid]); - } else { - pg.not_in_network.push(nodeid); - } - }); - - /** extract network data at 0.015 and subcluster thresholds - filter on dates subsequent to created date - **/ - - const my_nodeset = new Set(_.map(pg.node_objects, (n) => n.id)); - - const node_set15 = _.flatten( - hivtrace_cluster_depthwise_traversal( - json["Nodes"], - json["Edges"], - (e) => e.length <= 0.015, - null, - pg.node_objects - ) - ); - - const saved_traversal_edges = auto_extend ? [] : null; + self.recent_rapid_definition = function (network, date) { + date = date || self.get_reference_date(); + var subcluster_enum = [ + "No, dx>36 months", // 0 + "No, but dx≤12 months", + "Yes (dx≤12 months)", + "Yes (12 e.length <= self.subcluster_threshold, - saved_traversal_edges, - pg.node_objects + return { + depends: [helpers._networkCDCDateField], + label: "ClusterOI membership as of " + _defaultDateViewFormat(date), + enum: subcluster_enum, + //type: "String", + volatile: true, + color_scale: function () { + return d3.scale + .ordinal() + .domain(subcluster_enum.concat([_networkMissing])) + .range( + _.union( + [ + "steelblue", + "pink", + "red", + "#FF8C00", + "#9A4EAE", + "yellow", + "#FFFFFF", + "#FFD580", + ], + [_networkMissingColor] ) ); + }, - const direct_at_15 = new Set(); - - const json15 = _extract_single_cluster( - node_set15, - (e) => ( - e.length <= 0.015 && - (my_nodeset.has(json["Nodes"][e.target].id) || - my_nodeset.has(json["Nodes"][e.source].id)) - ), - //null, - true - ); + map: function (node) { + if (node.subcluster_label) { + if (node.priority_flag > 0) { + return subcluster_enum[node.priority_flag]; + } + return subcluster_enum[0]; + } + return subcluster_enum[6]; + }, + }; + }; - _.each(json15["Edges"], (e) => { - _.each([e.source, e.target], (nid) => { - if (!my_nodeset.has(json15["Nodes"][nid].id)) { - direct_at_15.add(json15["Nodes"][nid].id); - } - }); - }); + self._networkPredefinedAttributeTransforms = { + /** runtime computed node attributes, e.g. transforms of existing attributes */ - const current_time = self.today; - - const json_subcluster = _extract_single_cluster( - node_set_subcluster, - (e) => ( - e.length <= self.subcluster_threshold && - (my_nodeset.has(json["Nodes"][e.target].id) || - my_nodeset.has(json["Nodes"][e.source].id)) - /*|| (auto_extend && (self._filter_by_date( - pg.modified || pg.created, - timeDateUtil._networkCDCDateField, - current_time, - json["Nodes"][e.target], - true - ) || self._filter_by_date( - pg.modified || pg.created, - timeDateUtil._networkCDCDateField, - current_time, - json["Nodes"][e.source], - true - )))*/ - ), - true - ); + binned_vl_recent_value: { + depends: ["vl_recent_value"], + label: "binned_vl_recent_value", + enum: ["<200", "200-10000", ">10000"], + type: "String", + color_scale: function () { + return d3.scale + .ordinal() + .domain(["<200", "200-10000", ">10000", _networkMissing]) + .range(_.union(_networkSequentialColor[3], [_networkMissingColor])); + }, - const direct_subcluster = new Set(); - const direct_subcluster_new = new Set(); - _.each(json_subcluster["Edges"], (e) => { - _.each([e.source, e.target], (nid) => { - if (!my_nodeset.has(json_subcluster["Nodes"][nid].id)) { - direct_subcluster.add(json_subcluster["Nodes"][nid].id); + map: function (node) { + var vl_value = self.attribute_node_value_by_id( + node, + "vl_recent_value", + true + ); + if (vl_value !== _networkMissing) { + if (vl_value <= 200) { + return "<200"; + } + if (vl_value <= 10000) { + return "200-10000"; + } + return ">10000"; + } + return _networkMissing; + }, + }, - if ( - self._filter_by_date( - pg.modified || pg.created, - timeDateUtil._networkCDCDateField, - current_time, - json_subcluster["Nodes"][nid], - true - ) - ) - direct_subcluster_new.add(json_subcluster["Nodes"][nid].id); - } - }); - }); + binned_vl_recent_value_adj: { + depends: ["vl_recent_value_adj"], + label: "Most Recent Viral Load Category Binned", + enum: ["<200", "200-10000", ">10000"], + type: "String", + color_scale: function () { + return d3.scale + .ordinal() + .domain(["<200", "200-10000", ">10000", _networkMissing]) + .range(_.union(_networkSequentialColor[3], [_networkMissingColor])); + }, - pg.partitioned_nodes = _.map( - [ - [node_set15, direct_at_15], - [node_set_subcluster, direct_subcluster], - ], - (ns) => { - const nodesets = { - existing_direct: [], - new_direct: [], - existing_indirect: [], - new_indirect: [], - }; - - _.each(ns[0], (n) => { - if (my_nodeset.has(n.id)) return; - let key; - if ( - self._filter_by_date( - pg.modified || pg.created, - timeDateUtil._networkCDCDateField, - current_time, - n, - true - ) - ) { - key = "new"; - } else { - key = "existing"; - } + map: function (node) { + var vl_value = self.attribute_node_value_by_id( + node, + "vl_recent_value_adj", + true + ); + if (vl_value !== _networkMissing) { + if (vl_value <= 200) { + return "<200"; + } + if (vl_value <= 10000) { + return "200-10000"; + } + return ">10000"; + } + return _networkMissing; + }, + }, - if (ns[1].has(n.id)) { - key += "_direct"; - } else { - key += "_indirect"; - } + vl_result_interpretation: { + depends: ["vl_recent_value", "result_interpretation"], + label: "vl_result_interpretation", + color_stops: 6, + scale: d3.scale.log(10).domain([10, 1e6]).range([0, 5]), + category_values: ["Suppressed", "Viremic (above assay limit)"], + type: "Number-categories", + color_scale: function (attr) { + var color_scale_d3 = d3.scale + .linear() + .range([ + "#d53e4f", + "#fc8d59", + "#fee08b", + "#e6f598", + "#99d594", + "#3288bd", + ]) + .domain(_.range(_networkContinuousColorStops, -1, -1)); - nodesets[key].push(n); - }); + return function (v) { + if (_.isNumber(v)) { + return color_scale_d3(attr.scale(v)); + } + switch (v) { + case attr.category_values[0]: + return color_scale_d3(0); + case attr.category_values[1]: + return color_scale_d3(5); + default: + return _networkMissingColor; + } + }; + }, + label_format: d3.format(",.0f"), + map: function (node) { + var vl_value = self.attribute_node_value_by_id( + node, + "vl_recent_value", + true + ); + var result_interpretation = self.attribute_node_value_by_id( + node, + "result_interpretation" + ); - return nodesets; + if ( + vl_value !== _networkMissing || + result_interpretation !== _networkMissing + ) { + if (result_interpretation !== _networkMissing) { + if (result_interpretation === "<") { + return "Suppressed"; } - ); - - if (auto_extend && pg.tracking !== _cdcTrackingNone) { - const added_nodes = self.auto_expand_pg_handler(pg, nodeID2idx); - - if (added_nodes.size) { - _.each([...added_nodes], (nid) => { - const n = self.json.Nodes[nid]; - pg.nodes.push({ - name: n.id, - added: current_time, - kind: _cdcPrioritySetDefaultNodeKind, - autoadded: true, - }); - pg.node_objects.push(n); - }); - pg.validated = false; - pg.autoexpanded = true; - pg.pending = true; - pg.expanded = added_nodes.size; - pg.modified = self.today; + if (result_interpretation === ">") { + return "Viremic (above assay limit)"; + } + if (vl_value !== _networkMissing) { + return vl_value; } + } else { + return vl_value; } - - const node_set = new Set(_.map(pg.nodes, (n) => n.name)); - pg.meets_priority_def = _.some(priority_subclusters, (ps) => ( - _.filter([...ps], (psi) => node_set.has(psi)).length === ps.size - )); - const cutoff12 = _n_months_ago(self.get_reference_date(), 12); - pg.last12 = _.filter(pg.node_objects, (n) => - self._filter_by_date( - cutoff12, - timeDateUtil._networkCDCDateField, - self.today, - n, - false - ) - ).length; } - }); - } - }; - - self.priority_groups_update_node_sets = function (name, operation) { - // name : the name of the priority group being added - // operation: one of - // "insert" , "delete", "update" - - const sets = self.priority_groups_export().filter((pg) => pg.name === name); - const to_post = { - operation: operation, - name: name, - url: window.location.href, - sets: JSON.stringify(sets), - }; - if (self.priority_set_table_write && self.priority_set_table_writeable) { - d3.text(self.priority_set_table_write) - .header("Content-Type", "application/json") - .post(JSON.stringify(to_post), (error, data) => { - if (error) { - console.log("received fatal error:", error); - /* - $(".container").html( - '
FATAL ERROR. Please reload the page and contact help desk.
' - ); - */ - } - }); - } - }; + return _networkMissing; + }, + }, - self.priority_groups_compute_node_membership = function () { - const pg_nodesets = []; + //subcluster_or_priority_node_simple: self.recent_rapid_definition_simple, + //subcluster_or_priority_node: self.recent_rapid_definition, - _.each(self.defined_priority_groups, (g) => { - pg_nodesets.push([ - g.name, - g.createdBy === _cdcCreatedBySystem, - new Set(_.map(g.nodes, (n) => n.name)), - ]); - }); + /*subcluster_index: { + depends: [helpers._networkCDCDateField], + label: "Subcluster ID", + type: "String", - const pg_enum = [ - "Yes (dx≤12 months)", - "Yes (1236 months)", - "No", - ]; + map: function (node) { + return node.subcluster_label; + }, + },*/ - _.each( - { - subcluster_or_priority_node: { - depends: [timeDateUtil._networkCDCDateField], - label: _cdcPOImember, - enum: pg_enum, - type: "String", - volatile: true, - color_scale: function () { - return d3.scale - .ordinal() - .domain(pg_enum.concat([_networkMissing])) - .range([ - "red", - "orange", - "yellow", - "steelblue", - _networkMissingColor, - ]); - }, - map: function (node) { - const npcoi = _.some(pg_nodesets, (d) => d[1] && d[2].has(node.id)); - if (npcoi) { - const cutoffs = [ - _n_months_ago(self.get_reference_date(), 12), - _n_months_ago(self.get_reference_date(), 36), - ]; - - //const ysd = self.attribute_node_value_by_id( - // node, - // "years_since_dx" - //); - - if ( - self._filter_by_date( - cutoffs[0], - timeDateUtil._networkCDCDateField, - self.get_reference_date(), - node, - false - ) - ) - return pg_enum[0]; - if ( - self._filter_by_date( - cutoffs[1], - timeDateUtil._networkCDCDateField, - self.get_reference_date(), - node, - false - ) - ) - return pg_enum[1]; - return pg_enum[2]; - } - return pg_enum[3]; - }, - }, - cluster_uid: { - depends: [timeDateUtil._networkCDCDateField], - label: "Clusters of Interest", - type: "String", - volatile: true, - map: function (node) { - const memberships = _.filter(pg_nodesets, (d) => d[2].has(node.id)); - if (memberships.length === 1) { - return memberships[0][0]; - } else if (memberships.length > 1) { - return "Multiple"; - } - return "None"; - }, - }, - subcluster_id: { - depends: [timeDateUtil._networkCDCDateField], - label: "Subcluster ID", - type: "String", - //label_format: d3.format(".2f"), - map: function (node) { - if (node) { - return node.subcluster_label || "None"; - } - return _networkMissing; - }, - }, + age_dx_normalized: { + depends: ["age_dx"], + overwrites: "age_dx", + label: "Age at Diagnosis", + enum: ["<13", "13-19", "20-29", "30-39", "40-49", "50-59", "≥60"], + type: "String", + color_scale: function () { + return d3.scale + .ordinal() + .domain([ + "<13", + "13-19", + "20-29", + "30-39", + "40-49", + "50-59", + "≥60", + _networkMissing, + ]) + .range([ + "#b10026", + "#e31a1c", + "#fc4e2a", + "#fd8d3c", + "#feb24c", + "#fed976", + "#ffffb2", + "#636363", + ]); }, - self._aux_populated_predefined_attribute - ); - self._aux_populate_category_menus(); - }; - - self.priority_groups_edit_set_description = function ( - name, - description, - update_table - ) { - if (self.defined_priority_groups) { - var idx = _.findIndex( - self.defined_priority_groups, - (g) => g.name === name - ); - if (idx >= 0) { - self.defined_priority_groups[idx].description = description; - self.priority_groups_update_node_sets(name, "update"); - if (update_table) { - clustersOfInterest.draw_priority_set_table(self); + map: function (node) { + var vl_value = self.attribute_node_value_by_id(node, "age_dx"); + if (vl_value === ">=60") { + return "≥60"; } - } - } - }; - - self.priority_groups_remove_set = function (name, update_table) { - if (self.defined_priority_groups) { - var idx = _.findIndex( - self.defined_priority_groups, - (g) => g.name === name - ); - if (idx >= 0) { - self.defined_priority_groups.splice(idx, 1); - self.priority_groups_update_node_sets(name, "delete"); - if (update_table) { - clustersOfInterest.draw_priority_set_table(self); + if (vl_value === "\ufffd60") { + return "≥60"; } - } - } - }; + if (Number(vl_value) >= 60) { + return "≥60"; + } + return vl_value; + }, + }, - self.priority_groups_export = function (group_set, include_unvalidated) { - group_set = group_set || self.defined_priority_groups; - - return _.map( - _.filter(group_set, (g) => include_unvalidated || g.validated), - (g) => ({ - name: g.name, - description: g.description, - nodes: g.nodes, - modified: _defaultDateFormats[0](g.modified), - kind: g.kind, - created: _defaultDateFormats[0](g.created), - createdBy: g.createdBy, - tracking: g.tracking, - autocreated: g.autocreated, - autoexpanded: g.autoexpanded, - pending: g.pending, - }) - ); - }; + years_since_dx: { + depends: [helpers._networkCDCDateField], + label: "Years since diagnosis", + type: "Number", + label_format: d3.format(".2f"), + map: function (node) { + try { + var value = self._parse_dates( + self.attribute_node_value_by_id(node, helpers._networkCDCDateField) + ); - self.priority_groups_is_new_node = function (group_set, node) { - return node.autoadded; - }; + if (value) { + value = (self.today - value) / 31536000000; + } else { + value = _networkMissing; + } - self.priority_groups_export_nodes = function ( - group_set, - include_unvalidated - ) { - group_set = group_set || self.defined_priority_groups; - - return _.flatten( - _.map( - _.filter(group_set, (g) => include_unvalidated || g.validated), - (g) => { - //const refTime = g.modified.getTime(); - //console.log ("GROUP: ",g.name, " = ", g.modified); - - const exclude_nodes = new Set(g.not_in_network); - let cluster_detect_size = 0; - g.nodes.forEach((node) => { - if (node.added <= g.created) cluster_detect_size++; - }); - return _.map( - _.filter(g.nodes, (gn) => !exclude_nodes.has(gn.name)), - (gn) => ({ - eHARS_uid: gn.name, - cluster_uid: g.name, - cluster_ident_method: g.kind, - person_ident_method: gn.kind, - person_ident_dt: hivtrace_date_or_na_if_missing(gn.added), - new_linked_case: self.priority_groups_is_new_node(g, gn) - ? 1 - : 0, - cluster_created_dt: hivtrace_date_or_na_if_missing(g.created), - network_date: hivtrace_date_or_na_if_missing(self.today), - cluster_detect_size: cluster_detect_size, - cluster_type: g.createdBy, - cluster_modified_dt: hivtrace_date_or_na_if_missing(g.modified), - cluster_growth: _cdcConciseTrackingOptions[g.tracking], - national_priority: g.meets_priority_def, - cluster_current_size: g.nodes.length, - cluster_dx_recent12_mo: g.last12, - cluster_overlap: g.overlap.sets, - }) + return value; + } catch (err) { + return _networkMissing; + } + }, + color_scale: function (attr) { + var range_without_missing = _.without( + attr.value_range, + _networkMissing + ); + var color_scale = _.compose( + d3.interpolateRgb("#ffffcc", "#800026"), + d3.scale + .linear() + .domain([ + range_without_missing[0], + range_without_missing[range_without_missing.length - 1], + ]) + .range([0, 1]) + ); + return function (v) { + if (v === _networkMissing) { + return _networkMissingColor; + } + return color_scale(v); + }; + }, + }, + + hiv_aids_dx_dt_year: { + depends: [helpers._networkCDCDateField], + label: "Diagnosis Year", + type: "Number", + label_format: d3.format(".0f"), + map: function (node) { + try { + var value = self._parse_dates( + self.attribute_node_value_by_id(node, helpers._networkCDCDateField) ); + if (value) { + value = String(value.getFullYear()); + } else { + value = _networkMissing; + } + return value; + } catch (err) { + return _networkMissing; } - ) - ); + }, + color_scale: function (attr) { + var range_without_missing = _.without( + attr.value_range, + _networkMissing + ); + var color_scale = _.compose( + d3.interpolateRgb("#ffffcc", "#800026"), + d3.scale + .linear() + .domain([ + range_without_missing[0], + range_without_missing[range_without_missing.length - 1], + ]) + .range([0, 1]) + ); + return function (v) { + if (v === _networkMissing) { + return _networkMissingColor; + } + return color_scale(v); + }; + }, + }, }; - self.priority_groups_export_sets = function () { - return _.flatten( - _.map( - _.filter(self.defined_priority_groups, (g) => g.validated), - (g) => ({ - cluster_type: g.createdBy, - cluster_uid: g.name, - cluster_modified_dt: hivtrace_date_or_na_if_missing(g.modified), - cluster_created_dt: hivtrace_date_or_na_if_missing(g.created), - cluster_ident_method: g.kind, - cluster_growth: _cdcConciseTrackingOptions[g.tracking], - cluster_current_size: g.nodes.length, - national_priority: g.meets_priority_def, - cluster_dx_recent12_mo: g.last12, - cluster_overlap: g.overlap.sets, - }) - ) - ); - }; + if (self.cluster_attributes) { + self._networkPredefinedAttributeTransforms["_newly_added"] = { + label: "Compared to previous network", + enum: ["Existing", "New", "Moved clusters"], + type: "String", + map: function (node) { + if (node.attributes.indexOf("new_node") >= 0) { + return "New"; + } + if (node.attributes.indexOf("moved_clusters") >= 0) { + return "Moved clusters"; + } + return "Existing"; + }, + color_scale: function () { + return d3.scale + .ordinal() + .domain(["Existing", "New", "Moved clusters", _networkMissing]) + .range(["#7570b3", "#d95f02", "#1b9e77", "gray"]); + }, + }; + } - //--------------------------------------------------------------------------------------------------- - // END: NODE SET GROUPS - //--------------------------------------------------------------------------------------------------- + if (self.precomputed_subclusters) { + _.each(self.precomputed_subclusters, (v, k) => { + self._networkPredefinedAttributeTransforms["_subcluster" + k] = { + label: "Subcluster @" + d3.format("p")(Number(k)), + type: "String", + map: function (node) { + if ("subcluster" in node) { + var sub_at_k = _.find(node.subcluster, (t) => t[0] === k); + if (sub_at_k) { + return sub_at_k[1]; + } + } + return "Not in a subcluster"; + }, + }; + }); + } - //--------------------------------------------------------------------------------------------------- - // BEGIN: NODE SET EDITOR - //--------------------------------------------------------------------------------------------------- + if (options && options["computed-attributes"]) { + _.extend( + self._networkPredefinedAttributeTransforms, + options["computed-attributes"] + ); + } - clustersOfInterest.init(self); + self._parse_dates = function (value) { + if (value instanceof Date) { + return value; + } + var parsed_value = null; - //--------------------------------------------------------------------------------------------------- - // END: NODE SET EDITOR - //--------------------------------------------------------------------------------------------------- + var passed = _.any(_defaultDateFormats, (f) => { + parsed_value = f.parse(value); + return parsed_value; + }); - //--------------------------------------------------------------------------------------------------- + //console.log (value + " mapped to " + parsed_value); - self.node_shaper = { - id: null, - shaper: function () { - return "circle"; - }, + if (passed) { + if ( + self._is_CDC_ && + (parsed_value.getFullYear() < 1970 || + parsed_value.getFullYear() > _networkUpperBoundOnDate) + ) { + throw Error("Invalid date"); + } + return parsed_value; + } + + throw Error("Invalid date"); }; - nodesTab.init(d3.select(nodes_table)); + /*------------ Network layout code ---------------*/ + var handle_cluster_click = function (cluster, release) { + var container = d3.select(self.container); + var id = "d3_context_menu_id"; + var menu_object = container.select("#" + id); - self.filter_edges = true; - self.hide_hxb2 = false; - self.charge_correction = 5; - 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.cluster_table = d3.select(clusters_table); - self.priority_set_table = - self._is_CDC_ && options && options["priority-table"] - ? d3.select(options["priority-table"]) - : null; - self.priority_set_table_write = - self._is_CDC_ && options && options["priority-table-writeback"] - ? options["priority-table-writeback"] - : null; - self.needs_an_update = false; - self.hide_unselected = false; - self.show_percent_in_pairwise_table = false; - self.gradient_id = 0; - - self.priority_set_table_writeable = true; - - self._calc_country_nodes = function (calc_options) { - if (calc_options && "country-centers" in calc_options) { - self.mapProjection = d3.geo - .mercator() - .translate([ - self.margin.left + self.width / 2, - self.margin.top + self.height / 2, - ]) - .scale((150 * self.width) / 960); - _.each(self.countryCentersObject, (value) => { - value.countryXY = self.mapProjection([value.longt, value.lat]); - }); + if (menu_object.empty()) { + menu_object = container + .append("ul") + .attr("id", id) + .attr("class", "dropdown-menu") + .attr("role", "menu"); } - }; - if ( - options && - "country-centers" in options && - "country-outlines" in options - ) { - self.countryCentersObject = options["country-centers"]; - self.countryOutlines = options["country-outlines"]; - self._calc_country_nodes(options); - //console.log (self.countryCentersObject); - self.showing_on_map = options.showing_on_map; - //console.log (self.showing_on_map); - } else { - self.countryCentersObject = null; - self.showing_on_map = false; - } + menu_object.selectAll("li").remove(); - self._additional_node_pop_fields = []; - /** 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 */ + var already_fixed = cluster && cluster.fixed === 1; - if (options && "minimum size" in options) { - self.minimum_cluster_size = options["minimum size"]; - } else if (self._is_CDC_) { - self.minimum_cluster_size = 5; - } else { - self.minimum_cluster_size = 5; - } + 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"); + }); - timeDateUtil.init(options, self._is_CDC_, timeDateUtil._networkCDCDateField); + menu_object + .append("li") + .append("a") + .attr("tabindex", "-1") + .text("Center on screen") + .on("click", (d) => { + cluster.fixed = 0; + center_cluster_handler(cluster); + menu_object.style("display", "none"); + }); - if (self._is_CDC_) { - self._additional_node_pop_fields.push(timeDateUtil._networkCDCDateField); - } + 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 (options && "core-link" in options) { - self.core_link_length = options["core-link"]; - } else { - self.core_link_length = -1; - } + 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 (options && "edge-styler" in options) { - self.additional_edge_styler = options["edge-styler"]; - } else { - self.additional_edge_styler = null; - } + 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)); + }); + } - self.filter_by_size = function (cluster, value) { - return cluster.children.length >= self.minimum_cluster_size; - }; + // 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); - self.filter_singletons = function (cluster, value) { - return cluster.children.length > 1; - }; + 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 } + ); + }); + } - self.filter_if_added = function (cluster) { - return self.cluster_attributes[cluster.cluster_id].type !== "existing"; - }; + cluster.fixed = 1; - self.filter_time_period = function (cluster) { - return _.some(self.nodes_by_cluster[cluster.cluster_id], (n) => - self.attribute_node_value_by_id(n, timeDateUtil.getClusterTimeScale()) >= - self.using_time_filter - ); - }; + 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"); + } - self.cluster_filtering_functions = { - size: self.filter_by_size, - singletons: self.filter_singletons, + container.on( + "click", + (d) => { + handle_cluster_click(null, already_fixed ? null : cluster); + }, + true + ); }; - self.using_time_filter = null; + /*self._handle_inline_charts = function (e) { - if (self.json.Notes) { - _.each(self.json.Notes, (s) => (self.warning_string += s + "
")); - } + }*/ - 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._get_node_country = function (node) { + var countryCodeAlpha2 = self.attribute_node_value_by_id(node, "country"); + if (countryCodeAlpha2 === _networkMissing) { + countryCodeAlpha2 = self.attribute_node_value_by_id(node, "Country"); } - } - - self.cluster_display_filter = function (cluster) { - return _.every(self.cluster_filtering_functions, (filter) => filter(cluster)); + return countryCodeAlpha2; }; - self.initial_packed = - options && options["initial_layout"] === "tiled" ? false : true; + 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); - self.recent_rapid_definition_simple = function (network, date) { - // date = date || self.get_reference_date(); // not used + countries.enter().append("path"); + countries.exit().remove(); - var subcluster_enum_simple = [ - "Not a member of national priority clusterOI", // 1,4,5,6 - "12 months and in national priority clusterOI", // 2 - "36 months and in national priority clusterOI", // 3 - ">36 months and in national priority clusterOI", // 0 - ]; + self.countries_in_cluster = {}; - return { - depends: [timeDateUtil._networkCDCDateField], - label: "Subcluster or Priority Node", - enum: subcluster_enum_simple, - type: "String", - color_scale: function () { - return d3.scale - .ordinal() - .domain(subcluster_enum_simple.concat([_networkMissing])) - .range( - _.union( - ["#CCCCCC", "red", "blue", "#9A4EAE"], - [_networkMissingColor] - ) - ); - }, + _.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; + } + }); - map: function (node) { - if (node.subcluster_label) { - if (node.nationalCOI) { - return subcluster_enum_simple[node.nationalCOI]; + countries + .attr("class", "country") + .attr("d", path) + .attr("stroke", "saddlebrown") + .attr("fill", (d) => { + if (d.id in self.countries_in_cluster) { + return "navajowhite"; } - } - return subcluster_enum_simple[0]; - }, - }; + return "bisque"; + }) + .attr("stroke-width", (d) => { + if (d.id in self.countries_in_cluster) { + return 1.5; + } + return 0.5; + }); + } + return self; }; - self.recent_rapid_definition = function (network, date) { - date = date || self.get_reference_date(); - var subcluster_enum = [ - "No, dx>36 months", // 0 - "No, but dx≤12 months", - "Yes (dx≤12 months)", - "Yes (12 0) { - return subcluster_enum[node.priority_flag]; + network.handle_inline_charts = function (plot_filter) { + var attr = null; + var color = null; + if ( + network.colorizer["category_id"] && + !network.colorizer["continuous"] + ) { + var attr_desc = + network.json[_networkGraphAttrbuteID][ + network.colorizer["category_id"] + ]; + attr = {}; + attr[network.colorizer["category_id"]] = attr_desc["label"]; + color = {}; + color[attr_desc["label"]] = network.colorizer["category"]; } - return subcluster_enum[0]; + + svgPlots.plot_cluster_dynamics( + network.extract_network_time_series( + helpers.getClusterTimeScale(), + attr, + plot_filter + ), + network.network_cluster_dynamics, + "Quarter of Diagnosis", + "Number of Cases", + null, + null, + { + base_line: 20, + top: network.margin.top, + right: network.margin.right, + bottom: 3 * 20, + left: 5 * 20, + font_size: 12, + rect_size: 14, + width: network.width / 2, + height: network.height / 2, + colorizer: color, + prefix: network.dom_prefix, + barchart: true, + drag: { + x: network.width * 0.45, + y: 0, + }, + } + ); + }; + network.handle_inline_charts(); + if (e) { + e.text("Hide time-course plots"); } - return subcluster_enum[6]; - }, + } else { + if (e) { + e.text("Show time-course plots"); + } + network.network_cluster_dynamics.remove(); + network.network_cluster_dynamics = null; + network.handle_inline_charts = null; + } }; + + if (helpers.getClusterTimeScale()) { + if (export_items) { + export_items.push(["Show time-course plots", event_handler]); + } else { + event_handler(self); + } + } }; - self._networkPredefinedAttributeTransforms = { - /** runtime computed node attributes, e.g. transforms of existing attributes */ + 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(); + }; - binned_vl_recent_value: { - depends: ["vl_recent_value"], - label: "binned_vl_recent_value", - enum: ["<200", "200-10000", ">10000"], - type: "String", - color_scale: function () { - return d3.scale - .ordinal() - .domain(["<200", "200-10000", ">10000", _networkMissing]) - .range(_.union(_networkSequentialColor[3], [_networkMissingColor])); - }, + 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)); - map: function (node) { - var vl_value = self.attribute_node_value_by_id( - node, - "vl_recent_value", - true - ); - if (vl_value !== _networkMissing) { - if (vl_value <= 200) { - return "<200"; - } - if (vl_value <= 10000) { - return "200-10000"; - } - return ">10000"; - } - return _networkMissing; - }, - }, + if (!cluster) { + return; + } - binned_vl_recent_value_adj: { - depends: ["vl_recent_value_adj"], - label: "Most Recent Viral Load Category Binned", - enum: ["<200", "200-10000", ">10000"], - type: "String", - color_scale: function () { - return d3.scale - .ordinal() - .domain(["<200", "200-10000", ">10000", _networkMissing]) - .range(_.union(_networkSequentialColor[3], [_networkMissingColor])); - }, + additional_options = additional_options || {}; - map: function (node) { - var vl_value = self.attribute_node_value_by_id( - node, - "vl_recent_value_adj", - true - ); - if (vl_value !== _networkMissing) { - if (vl_value <= 200) { - return "<200"; - } - if (vl_value <= 10000) { - return "200-10000"; - } - return ">10000"; - } - return _networkMissing; - }, - }, + additional_options["parent_graph"] = self; - vl_result_interpretation: { - depends: ["vl_recent_value", "result_interpretation"], - label: "vl_result_interpretation", - color_stops: 6, - scale: d3.scale.log(10).domain([10, 1e6]).range([0, 5]), - category_values: ["Suppressed", "Viremic (above assay limit)"], - type: "Number-categories", - color_scale: function (attr) { - var color_scale_d3 = d3.scale - .linear() - .range([ - "#d53e4f", - "#fc8d59", - "#fee08b", - "#e6f598", - "#99d594", - "#3288bd", - ]) - .domain(_.range(_networkContinuousColorStops, -1, -1)); + var filtered_json = self._extract_single_cluster( + custom_filter + ? _.filter(self.json.Nodes, custom_filter) + : cluster.children, + null, + null, + null, + include_injected_edges + ); - return function (v) { - if (_.isNumber(v)) { - return color_scale_d3(attr.scale(v)); - } - switch (v) { - case attr.category_values[0]: - return color_scale_d3(0); - case attr.category_values[1]: - return color_scale_d3(5); - default: - return _networkMissingColor; - } - }; - }, - label_format: d3.format(",.0f"), - map: function (node) { - var vl_value = self.attribute_node_value_by_id( - node, - "vl_recent_value", - true - ); - var result_interpretation = self.attribute_node_value_by_id( - node, - "result_interpretation" - ); + if (_networkGraphAttrbuteID in json) { + filtered_json[_networkGraphAttrbuteID] = {}; + $.extend( + true, + filtered_json[_networkGraphAttrbuteID], + json[_networkGraphAttrbuteID] + ); + } - if ( - vl_value !== _networkMissing || - result_interpretation !== _networkMissing - ) { - if (result_interpretation !== _networkMissing) { - if (result_interpretation === "<") { - return "Suppressed"; - } - if (result_interpretation === ">") { - return "Viremic (above assay limit)"; - } - if (vl_value !== _networkMissing) { - return vl_value; - } - } else { - return vl_value; - } - } + 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() + ) + ); + }, + ]); + } - return _networkMissing; - }, - }, + //self._check_for_time_series(export_items); - //subcluster_or_priority_node_simple: self.recent_rapid_definition_simple, - //subcluster_or_priority_node: self.recent_rapid_definition, + 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, + }, + }); + } - /*subcluster_index: { - depends: [timeDateUtil._networkCDCDateField], - label: "Subcluster ID", - type: "String", + return self.open_exclusive_tab_view_aux( + filtered_json, + custom_name ? custom_name(cluster_id) : "Cluster " + cluster_id, + additional_options + ); + }; - map: function (node) { - return node.subcluster_label; - }, - },*/ + self._random_id = function (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; + }; - age_dx_normalized: { - depends: ["age_dx"], - overwrites: "age_dx", - label: "Age at Diagnosis", - enum: ["<13", "13-19", "20-29", "30-39", "40-49", "50-59", "≥60"], - type: "String", - color_scale: function () { - return d3.scale - .ordinal() - .domain([ - "<13", - "13-19", - "20-29", - "30-39", - "40-49", - "50-59", - "≥60", - _networkMissing, - ]) - .range([ - "#b10026", - "#e31a1c", - "#fc4e2a", - "#fd8d3c", - "#feb24c", - "#fed976", - "#ffffb2", - "#636363", - ]); - }, - map: function (node) { - var vl_value = self.attribute_node_value_by_id(node, "age_dx"); - if (vl_value === ">=60") { - return "≥60"; - } - if (vl_value === "\ufffd60") { - return "≥60"; - } - if (Number(vl_value) >= 60) { - return "≥60"; - } - return vl_value; - }, - }, + self.open_exclusive_tab_view_aux = function ( + filtered_json, + title, + option_extras + ) { + var letters = ["a", "b", "c", "d", "e", "f", "g"]; - years_since_dx: { - depends: [timeDateUtil._networkCDCDateField], - label: "Years since diagnosis", - type: "Number", - label_format: d3.format(".2f"), - map: function (node) { - try { - var value = self._parse_dates( - self.attribute_node_value_by_id(node, timeDateUtil._networkCDCDateField) - ); + var random_prefix = self._random_id(letters, 32); + var random_tab_id = random_prefix + "_tab"; + var random_content_id = random_prefix + "_div"; + var random_button_bar = random_prefix + "_ui"; - if (value) { - value = (self.today - value) / 31536000000; - } else { - value = _networkMissing; - } + while ( + $("#" + random_tab_id).length || + $("#" + random_content_id).length || + $("#" + random_button_bar).length + ) { + random_prefix = self._random_id(letters, 32); + random_tab_id = random_prefix + "_tab"; + random_content_id = random_prefix + "_div"; + random_button_bar = random_prefix + "_ui"; + } - return value; - } catch (err) { - return _networkMissing; - } - }, - color_scale: function (attr) { - var range_without_missing = _.without( - attr.value_range, - _networkMissing - ); - var color_scale = _.compose( - d3.interpolateRgb("#ffffcc", "#800026"), - d3.scale - .linear() - .domain([ - range_without_missing[0], - range_without_missing[range_without_missing.length - 1], - ]) - .range([0, 1]) + 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 = $("
  • ").attr("id", random_tab_id); + + var new_link = $("") + .attr("href", "#" + random_content_id) + .attr("data-toggle", "tab") + .text(title); + $( + '' + ) + .appendTo(new_link) + .on("click", () => { + self.open_exclusive_tab_close( + random_tab_id, + random_content_id, + go_here_when_closed ); - return function (v) { - if (v === _networkMissing) { - return _networkMissingColor; - } - return color_scale(v); - }; - }, - }, + }); - hiv_aids_dx_dt_year: { - depends: [timeDateUtil._networkCDCDateField], - label: "Diagnosis Year", - type: "Number", - label_format: d3.format(".0f"), - map: function (node) { - try { - var value = self._parse_dates( - self.attribute_node_value_by_id(node, timeDateUtil._networkCDCDateField) - ); - if (value) { - value = String(value.getFullYear()); - } else { - value = _networkMissing; - } - return value; - } catch (err) { - return _networkMissing; - } - }, - color_scale: function (attr) { - var range_without_missing = _.without( - attr.value_range, - _networkMissing - ); - var color_scale = _.compose( - d3.interpolateRgb("#ffffcc", "#800026"), - d3.scale - .linear() - .domain([ - range_without_missing[0], - range_without_missing[range_without_missing.length - 1], - ]) - .range([0, 1]) - ); - return function (v) { - if (v === _networkMissing) { - return _networkMissingColor; - } - return color_scale(v); - }; - }, - }, - }; + new_link.appendTo(new_tab_header); + $("#" + tab_container).append(new_tab_header); - if (self.cluster_attributes) { - self._networkPredefinedAttributeTransforms["_newly_added"] = { - label: "Compared to previous network", - enum: ["Existing", "New", "Moved clusters"], - type: "String", - map: function (node) { - if (node.attributes.indexOf("new_node") >= 0) { - return "New"; - } - if (node.attributes.indexOf("moved_clusters") >= 0) { - return "Moved clusters"; - } - return "Existing"; - }, - color_scale: function () { - return d3.scale - .ordinal() - .domain(["Existing", "New", "Moved clusters", _networkMissing]) - .range(["#7570b3", "#d95f02", "#1b9e77", "gray"]); - }, - }; - } + var new_tab_content = $("
    ") + .addClass("tab-pane") + .attr("id", random_content_id) + .data("cluster", option_extras.cluster_id); - if (self.precomputed_subclusters) { - _.each(self.precomputed_subclusters, (v, k) => { - self._networkPredefinedAttributeTransforms["_subcluster" + k] = { - label: "Subcluster @" + d3.format("p")(Number(k)), - type: "String", - map: function (node) { - if ("subcluster" in node) { - var sub_at_k = _.find(node.subcluster, (t) => t[0] === k); - if (sub_at_k) { - return sub_at_k[1]; - } - } - return "Not in a subcluster"; - }, - }; - }); - } + if (option_extras.type === "subcluster") { + new_tab_content + .addClass("subcluster-view") + .addClass("subcluster-" + option_extras.cluster_id.replace(".", "_")); + } - if (options && options["computed-attributes"]) { - _.extend( - self._networkPredefinedAttributeTransforms, - options["computed-attributes"] - ); - } + //
  • Attributes
  • + 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); - self._parse_dates = function (value) { - if (value instanceof Date) { - return value; + new_button_bar.appendTo(new_tab_content); } - var parsed_value = null; + new_tab_content.appendTo("#" + content_container); - var passed = _.any(_defaultDateFormats, (f) => { - parsed_value = f.parse(value); - return parsed_value; + $(new_link).on("show.bs.tab", (e) => { + //console.log (e); + if (e.relatedTarget) { + //console.log (e.relatedTarget); + go_here_when_closed = e.relatedTarget; + } }); - //console.log (value + " mapped to " + parsed_value); + // 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 (passed) { if ( - self._is_CDC_ && - (parsed_value.getFullYear() < 1970 || - parsed_value.getFullYear() > _networkUpperBoundOnDate) + option_extras.showing_on_map && + self.countryCentersObject && + self.countryOutlines ) { - throw Error("Invalid date"); + 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(); + }; + } } - return parsed_value; - } - throw Error("Invalid date"); + 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; }; - /*------------ Network layout code ---------------*/ - var handle_cluster_click = function (cluster, release) { + // 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); @@ -2570,28 +2175,16 @@ var hivtrace_cluster_network_graph = function ( menu_object.selectAll("li").remove(); - var already_fixed = cluster && cluster.fixed === 1; - - 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"); - }); - + if (node) { + node.fixed = 1; menu_object .append("li") .append("a") .attr("tabindex", "-1") - .text("Center on screen") + .text(__("clusters_main")["collapse_cluster"]) .on("click", (d) => { - cluster.fixed = 0; - center_cluster_handler(cluster); + node.fixed = 0; + collapse_cluster_handler(node, true); menu_object.style("display", "none"); }); @@ -2599,66 +2192,38 @@ var hivtrace_cluster_network_graph = function ( .append("li") .append("a") .attr("tabindex", "-1") - .text((d) => { - if (cluster.fixed) return "Allow cluster to float"; - return "Hold cluster at current position"; - }) + .text((d) => node.show_label ? "Hide text label" : "Show text label") .on("click", (d) => { - cluster.fixed = !cluster.fixed; + 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 (self.isPrimaryGraph) { + if (clustersOfInterest.get_editor()) { menu_object .append("li") .append("a") .attr("tabindex", "-1") - .text((d) => "Show this cluster in separate tab") + .text((d) => "Add this node to the cluster of interest") .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)); + clustersOfInterest.get_editor().append_node(node.id, true); }); } - // 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 } - ); - }); - } + // SW20180605 : To be implemented - cluster.fixed = 1; + //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") @@ -2666,197 +2231,177 @@ var hivtrace_cluster_network_graph = function ( .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(null, already_fixed ? null : cluster); + handle_node_click(null); }, true ); }; - /*self._handle_inline_charts = function (e) { + function get_initial_xy(packed) { + // create clusters from nodes + var mapped_clusters = get_all_clusters(self.nodes); - }*/ + var d_clusters = { + id: "root", + children: [], + }; - self._get_node_country = function (node) { - var countryCodeAlpha2 = self.attribute_node_value_by_id(node, "country"); - if (countryCodeAlpha2 === _networkMissing) { - countryCodeAlpha2 = self.attribute_node_value_by_id(node, "Country"); + // filter out clusters that are to be excluded + if (self.exclude_cluster_ids) { + mapped_clusters = _.omit(mapped_clusters, self.exclude_cluster_ids); } - 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); + d_clusters.children = _.map(mapped_clusters, (value, key) => ({ + cluster_id: key, + children: value, + })); - countries.enter().append("path"); - countries.exit().remove(); + 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); - self.countries_in_cluster = {}; + var clusters = treemap.nodes(d_clusters); + _.each(clusters, (c) => { + //c.fixed = true; + }); + return clusters; + } - _.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; - } - }); + function prepare_data_to_graph() { + var graphMe = {}; + graphMe.all = []; + graphMe.edges = []; + graphMe.nodes = []; + graphMe.clusters = []; - 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; + 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; } - return 0.5; - }); - } - return self; - }; + } + } + }); - self._check_for_time_series = function (export_items) { - var event_handler = function (network, e) { - if (e) { - e = d3.select(e); + 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); } - if (!network.network_cluster_dynamics) { - network.network_cluster_dynamics = network.network_svg - .append("g") - .attr("id", self.dom_prefix + "-dynamics-svg") - .attr("transform", "translate (" + network.width * 0.45 + ",0)"); + }); - network.handle_inline_charts = function (plot_filter) { - var attr = null; - var color = null; - if ( - network.colorizer["category_id"] && - !network.colorizer["continuous"] - ) { - var attr_desc = - network.json[_networkGraphAttrbuteID][ - network.colorizer["category_id"] - ]; - attr = {}; - attr[network.colorizer["category_id"]] = attr_desc["label"]; - color = {}; - color[attr_desc["label"]] = network.colorizer["category"]; + 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]; } - misc.cluster_dynamics( - network.extract_network_time_series( - timeDateUtil.getClusterTimeScale(), - attr, - plot_filter - ), - network.network_cluster_dynamics, - "Quarter of Diagnosis", - "Number of Cases", - null, - null, - { - base_line: 20, - top: network.margin.top, - right: network.margin.right, - bottom: 3 * 20, - left: 5 * 20, - font_size: 12, - rect_size: 14, - width: network.width / 2, - height: network.height / 2, - colorizer: color, - prefix: network.dom_prefix, - barchart: true, - drag: { - x: network.width * 0.45, - y: 0, - }, - } - ); - }; - network.handle_inline_charts(); - if (e) { - e.text("Hide time-course plots"); - } - } else { - if (e) { - e.text("Show time-course plots"); + y.source = drawnNodes[x.source]; + y.target = drawnNodes[x.target]; + y.ref = x; + graphMe.edges.push(y); } - network.network_cluster_dynamics.remove(); - network.network_cluster_dynamics = null; - network.handle_inline_charts = null; } - }; + }); - if (timeDateUtil.getClusterTimeScale()) { - if (export_items) { - export_items.push(["Show time-course plots", event_handler]); - } else { - event_handler(self); - } - } - }; + return graphMe; + } - 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._refresh_subcluster_view = function (set_date) { + self.annotate_priority_clusters(helpers._networkCDCDateField, 36, 12, set_date); + + var field_def = self.recent_rapid_definition(self, set_date); + + //console.log (field_def.dimension); + + if (field_def) { + _.each(self.nodes, (node) => { + const attr_v = field_def["map"](node, self); + 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.open_exclusive_tab_view = function ( - cluster_id, + self.view_subcluster = function ( + cluster, custom_filter, custom_name, - additional_options, - include_injected_edges + view_sub_options, + custom_edge_filter, + include_injected_edges, + length_threshold ) { - var cluster = _.find(self.clusters, (c) => String(c.cluster_id) === String(cluster_id)); - - if (!cluster) { - return; + 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); + } } - - additional_options = additional_options || {}; - - additional_options["parent_graph"] = self; - - var filtered_json = _extract_single_cluster( - custom_filter - ? _.filter(self.json.Nodes, custom_filter) - : cluster.children, - null, - null, + 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 (_networkGraphAttrbuteID in json) { filtered_json[_networkGraphAttrbuteID] = {}; $.extend( @@ -2866,3768 +2411,2284 @@ var hivtrace_cluster_network_graph = function ( ); } - var export_items = []; + 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", _defaultDateViewFormatSlider(self.today)) + .attr("max", _defaultDateViewFormatSlider(self.today)) + .attr( + "min", + _defaultDateViewFormatSlider( + d3.min(network.nodes, (node) => network.attribute_node_value_by_id( + node, + helpers._networkCDCDateField + )) + ) + ) + .on("change", function (e) { + //d3.event.preventDefault(); + var set_date = _defaultDateViewFormatSlider.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) { - export_items.push([ + extra_menu_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() + network._extract_attributes_for_nodes( + network._extract_nodes_by_id("1.1"), + network._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); - }); + 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 { - _.extend(additional_options, { - extra_menu: { - title: "Action", - items: export_items, - }, - }); + view_sub_options["extra_menu"] = { + title: "Action", + items: extra_menu_items, + }; } - return self.open_exclusive_tab_view_aux( + //self._check_for_time_series(extra_menu_items); + var cluster_view = self.open_exclusive_tab_view_aux( filtered_json, - custom_name ? custom_name(cluster_id) : "Cluster " + cluster_id, - additional_options + custom_name || "Subcluster " + cluster.cluster_id, + view_sub_options ); - }; - - self._random_id = function (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; - }; - - self.open_exclusive_tab_view_aux = function ( - filtered_json, - title, - option_extras - ) { - var letters = ["a", "b", "c", "d", "e", "f", "g"]; - - var random_prefix = self._random_id(letters, 32); - 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 = self._random_id(letters, 32); - 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"; + if (!view_sub_options.skip_recent_rapid) + cluster_view.handle_attribute_categorical("subcluster_or_priority_node"); + return cluster_view; - // add new tab to the menu bar and switch to it - var new_tab_header = $("
  • ").attr("id", random_tab_id); + /*var selector = + ".subcluster-" + + cluster.id.replace(".", "_") + + " .show-small-clusters-button"; - var new_link = $("") - .attr("href", "#" + random_content_id) - .attr("data-toggle", "tab") - .text(title); - $( - '' + var item = $( + 'View Parent' ) - .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); + .data("cluster_id", cluster.parent_cluster.cluster_id) + .insertAfter(selector); - var new_tab_content = $("
    ") - .addClass("tab-pane") - .attr("id", random_content_id) - .data("cluster", option_extras.cluster_id); + item.on("click", function(e) { + self.open_exclusive_tab_view($(this).data("cluster_id")); + });*/ + }; - if (option_extras.type === "subcluster") { - new_tab_content - .addClass("subcluster-view") - .addClass("subcluster-" + option_extras.cluster_id.replace(".", "_")); - } + var oldest_nodes_first = function (n1, n2) { + const date_field = date_field || helpers._networkCDCDateField; - //
  • Attributes
  • - 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); + // 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); - new_button_bar.appendTo(new_tab_content); + if (node1_dx === node2_dx) { + return n1.id < n2.id ? -1 : 1; } - new_tab_content.appendTo("#" + content_container); + return node1_dx < node2_dx ? -1 : 1; + }; - $(new_link).on("show.bs.tab", (e) => { - //console.log (e); - if (e.relatedTarget) { - //console.log (e.relatedTarget); - go_here_when_closed = e.relatedTarget; + self._filter_by_date = function ( + cutoff, + date_field, + start_date, + node, + count_newly_added + ) { + if (count_newly_added && self._is_new_node(node)) { + return true; + } + var node_dx = self.attribute_node_value_by_id(node, date_field); + if (node_dx instanceof Date) { + return node_dx >= cutoff && node_dx <= start_date; + } + try { + node_dx = self._parse_dates(node_dx); + if (node_dx instanceof Date) { + return node_dx >= cutoff && node_dx <= start_date; } - }); + } catch { + return undefined; + } + return false; + }; - // show the new tab - $(new_link).tab("show"); + 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 1236 months + */ - var cluster_view; + try { + start_date = start_date || self.get_reference_date(); - 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, - }; + var cutoff_long = helpers.getNMonthsAgo(start_date, span_months); + var cutoff_short = helpers.getNMonthsAgo(start_date, recent_months); - if (option_extras) { - _.extend(cluster_options, option_extras); + var node_iterator; + + if (start_date === self.today) { + node_iterator = self.nodes; + } else { + var beginning_of_time = helpers.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; + } + }); } - 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; + // extract all clusters at once to avoid inefficiencies of multiple edge-set traversals - // 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(); - }; + var split_clusters = {}; + var node_id_to_local_cluster = {}; - cluster_options["extra-graphics"] = _.wrap( - draw_map, - cluster_options["extra-graphics"] - ); - } else { - cluster_options["extra-graphics"] = function (network) { - return network._draw_topomap(); - }; + // 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); } - } + }); - cluster_options["today"] = self.today; + _.each(self.edges, (edge) => { + if (edge.length <= self.subcluster_threshold) { + var edge_cluster = self.nodes[edge.source].cluster; - cluster_view = hivtrace_cluster_network_graph( - filtered_json, - "#" + random_content_id, - null, - null, - random_button_bar, - attributes, - null, - null, - null, - parent_container, - cluster_options - ); + var source_id = self.nodes[edge.source].id; + var target_id = self.nodes[edge.target].id; - 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 ( + 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); + } } - } + }); - if (self.node_shaper["id"]) { - cluster_view.handle_shape_categorical(self.node_shaper["id"]); - } + const cluster_id_match = + self.precomputed_subclusters && + self.subcluster_threshold in self.precomputed_subclusters + ? self.precomputed_subclusters + : null; - if (self.colorizer["opacity_id"]) { - cluster_view.handle_attribute_opacity(self.colorizer["opacity_id"]); - } + _.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 */ - cluster_view.expand_cluster_handler(cluster_view.clusters[0], true); - } else { - return new_tab_content.attr("id"); - } - return cluster_view; - }; + //var cluster_nodes = self._extract_single_cluster (cluster.children, null, true); - // ensure all checkboxes are unchecked at initialization - $('input[type="checkbox"]').prop("checked", false); + var array_index = self.cluster_mapping[cluster_index]; - 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); + self.clusters[array_index].priority_score = 0; - if (menu_object.empty()) { - menu_object = container - .append("ul") - .attr("id", id) - .attr("class", "dropdown-menu") - .attr("role", "menu"); - } + var edges = []; - menu_object.selectAll("li").remove(); + /** 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 + ); - 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"); - }); + /** all edge sets with more than one edge */ + edges = _.filter(edges, (es) => es.length > 1); - 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"); + /** sort subclusters by oldest node */ + _.each(subclusters, (c, i) => { + c.sort(oldest_nodes_first); }); - 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); - }); - } + subclusters.sort((c1, c2) => oldest_nodes_first(c1[0], c2[0])); - // SW20180605 : To be implemented + let next_id = subclusters.length + 1; - //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"); - // }); + subclusters = _.map(subclusters, (c, i) => { + let subcluster_id = i + 1; - 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"); - } + 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(_networkSubclusterSeparator)[1] + ] = 1; + return; + } + } - container.on( - "click", - (d) => { - handle_node_click(null); - }, - true - ); - }; + precomputed_values[null] = 1; + }); - function get_initial_xy(packed) { - // create clusters from nodes - var mapped_clusters = get_all_clusters(self.nodes); + if ( + null in precomputed_values || + _.keys(precomputed_values).length !== 1 + ) { + subcluster_id = next_id++; + } else { + subcluster_id = _.keys(precomputed_values)[0]; + } - var d_clusters = { - id: "root", - children: [], - }; + /*if ((i+1) !== 0 + subcluster_id) { + console.log (self.clusters[array_index].cluster_id, i, "=>", subcluster_id, _.keys(precomputed_values)); + }*/ + } - // filter out clusters that are to be excluded - if (self.exclude_cluster_ids) { - mapped_clusters = _.omit(mapped_clusters, self.exclude_cluster_ids); - } + var label = + self.clusters[array_index].cluster_id + + _networkSubclusterSeparator + + subcluster_id; - d_clusters.children = _.map(mapped_clusters, (value, key) => ({ - cluster_id: key, - children: value, - })); + _.each(c, (n) => { + //if (!("subcluster_label" in n)) { + n.subcluster_label = label; + //} + n.priority_flag = 0; + }); - 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); + return { + children: _.clone(c), + parent_cluster: self.clusters[array_index], + cluster_id: label, + distances: helpers.describe_vector( + _.map(edges[i], (e) => e.length) + ), + }; + }); - var clusters = treemap.nodes(d_clusters); - _.each(clusters, (c) => { - //c.fixed = true; - }); - return clusters; - } + _.each(subclusters, (c) => { + _compute_cluster_degrees(c); + }); - function prepare_data_to_graph() { - var graphMe = {}; - graphMe.all = []; - graphMe.edges = []; - graphMe.nodes = []; - graphMe.clusters = []; + self.clusters[array_index].subclusters = subclusters; - var expandedClusters = []; - var drawnNodes = []; + /** now, for each subcluster, extract the recent and rapid part */ - 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; - } - } - } - }); + /** 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 - 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); - } - }); + 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 + */ - 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]; - } + _.each(subclusters, (sub) => { + // extract nodes based on dates - y.source = drawnNodes[x.source]; - y.target = drawnNodes[x.target]; - y.ref = x; - graphMe.edges.push(y); - } - } - }); + const date_filter = (n) => + self._filter_by_date(cutoff_long, date_field, start_date, n); - return graphMe; - } + var subcluster_json = self._extract_single_cluster( + _.filter(sub.children, date_filter), + null, + true, + cluster_nodes + ); - self._refresh_subcluster_view = function (set_date) { - self.annotate_priority_clusters(timeDateUtil._networkCDCDateField, 36, 12, set_date); + 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 + ); - var field_def = self.recent_rapid_definition(self, set_date); + sub.rr_count = rr_cluster.length; - //console.log (field_def.dimension); + rr_cluster.sort((a, b) => b.length - a.length); - if (field_def) { - _.each(self.nodes, (node) => { - const attr_v = field_def["map"](node, self); - inject_attribute_node_value_by_id( - node, - "subcluster_temporal_view", - attr_v - ); - }); + sub.priority_score = []; + sub.recent_nodes = []; - 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"); - } - }; + const future_date = new Date(start_date.getTime() + 1e13); - 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 = _extract_single_cluster( - nodes, - custom_edge_filter || - ((e) => e.length <= length_threshold), - false, - null, - include_injected_edges - ); + _.each(rr_cluster, (recent_cluster) => { + var priority_nodes = _.groupBy(recent_cluster, (n) => + self._filter_by_date(cutoff_short, date_field, start_date, n) + ); - _.each(filtered_json.Nodes, (n) => { - n.subcluster_label = "1.1"; - }); + 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 (_networkGraphAttrbuteID in json) { - filtered_json[_networkGraphAttrbuteID] = {}; - $.extend( - true, - filtered_json[_networkGraphAttrbuteID], - json[_networkGraphAttrbuteID] - ); - } + 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; - view_sub_options = view_sub_options || {}; + if (meets_priority_def) { + if (n.priority_flag === 1) { + n.priority_flag = 2; + } + n.nationalCOI = 1; + } + }); + } - view_sub_options["parent_graph"] = self; + if (false in priority_nodes) { + // not recent + _.each(priority_nodes[false], (n) => { + n.priority_flag = 3; - 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", _defaultDateViewFormatSlider(self.today)) - .attr("max", _defaultDateViewFormatSlider(self.today)) - .attr( - "min", - _defaultDateViewFormatSlider( - d3.min(network.nodes, (node) => network.attribute_node_value_by_id( - node, - timeDateUtil._networkCDCDateField - )) - ) - ) - .on("change", function (e) { - //d3.event.preventDefault(); - var set_date = _defaultDateViewFormatSlider.parse(this.value); - if (this.value) { - network._refresh_subcluster_view(set_date); + 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; + } + }); + } + }); - 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() - ) - ); - }, - ]); + //console.log (sub.recent_nodes); + self.clusters[array_index].priority_score = sub.priority_score; + }); + }); + } catch (err) { + console.log(err); } + }; - 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, - }; - } + function default_layout(packed) { + // let's create an array of clusters from the json - //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 init_layout = get_initial_xy(packed); - /*var selector = - ".subcluster-" + - cluster.id.replace(".", "_") + - " .show-small-clusters-button"; + 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 item = $( - 'View Parent' - ) - .data("cluster_id", cluster.parent_cluster.cluster_id) - .insertAfter(selector); + //var sizes = network_layout.size(); - item.on("click", function(e) { - self.open_exclusive_tab_view($(this).data("cluster_id")); - });*/ - }; + 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; + }; - function _n_months_ago(reference_date, months) { - var past_date = new Date(reference_date); - var past_months = past_date.getMonth(); - var diff_year = Math.floor(months / 12); - var left_over = months - diff_year * 12; + _.each([self.nodes, self.clusters], (list) => { + _.each(list, set_init_coords); + }); - if (left_over > past_months) { - past_date.setFullYear(past_date.getFullYear() - diff_year - 1); - past_date.setMonth(12 - (left_over - past_months)); - } else { - past_date.setFullYear(past_date.getFullYear() - diff_year); - past_date.setMonth(past_months - left_over); - } + self.clusters.forEach(self.collapse_cluster); + } - //past_date.setTime (past_date.getTime () - months * 30 * 24 * 3600000); - return past_date; + function change_spacing(delta) { + self.charge_correction *= delta; + network_layout.start(); } - var oldest_nodes_first = function (n1, n2) { - const date_field = date_field || timeDateUtil._networkCDCDateField; + 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; - // 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); + self.width += delta; + self.height += delta; - if (node1_dx === node2_dx) { - return n1.id < n2.id ? -1 : 1; - } - return node1_dx < node2_dx ? -1 : 1; - }; + 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) + ); - self._filter_by_date = function ( - cutoff, - date_field, - start_date, - node, - count_newly_added - ) { - if (count_newly_added && self._is_new_node(node)) { - return true; - } - var node_dx = self.attribute_node_value_by_id(node, date_field); - if (node_dx instanceof Date) { - return node_dx >= cutoff && node_dx <= start_date; + _.each(network_layout.nodes(), (node) => { + node.x = rescale_x(node.x); + node.y = rescale_y(node.y); + }); } - try { - node_dx = self._parse_dates( - self.attribute_node_value_by_id(node, date_field) - ); - if (node_dx instanceof Date) { - return node_dx >= cutoff && node_dx <= start_date; - } - } catch (err) { - return undefined; + + 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); } - return false; - }; + } - 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 1236 months - */ - - try { - start_date = start_date || self.get_reference_date(); + self.compute_adjacency_list = _.once(() => { + self.nodes.forEach((n) => { + n.neighbors = d3.set(); + }); - var cutoff_long = _n_months_ago(start_date, span_months); - var cutoff_short = _n_months_ago(start_date, recent_months); + self.edges.forEach((e) => { + self.nodes[e.source].neighbors.add(e.target); + self.nodes[e.target].neighbors.add(e.source); + }); + }); - var node_iterator; + self.compute_local_clustering_coefficients = _.once(() => { + self.compute_adjacency_list(); - 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; + self.nodes.forEach((n) => { + _.defer((a_node) => { + const neighborhood_size = a_node.neighbors.size(); + if (neighborhood_size < 2) { + a_node.lcc = helpers.HIVTRACE_UNDEFINED; + } else if (neighborhood_size > 500) { + a_node.lcc = helpers.HIVTRACE_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++; + } + } } - }); - } - // 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); + a_node.lcc = + (2 * counter) / neighborhood_size / (neighborhood_size - 1); } - }); - - _.each(self.edges, (edge) => { - if (edge.length <= self.subcluster_threshold) { - var edge_cluster = self.nodes[edge.source].cluster; + }, n); + }); + }); - var source_id = self.nodes[edge.source].id; - var target_id = self.nodes[edge.target].id; + self.get_node_by_id = function (id) { + return self.nodes.filter((n) => n.id === id)[0]; + }; - if ( - source_id in node_id_to_local_cluster && - target_id in node_id_to_local_cluster - ) { - var copied_edge = _.clone(edge); + self.compute_local_clustering_coefficients_worker = _.once(() => { + var worker = new Worker("workers/lcc.js"); - copied_edge.source = node_id_to_local_cluster[source_id]; - copied_edge.target = node_id_to_local_cluster[target_id]; + worker.onmessage = function (event) { + var nodes = event.data.Nodes; - split_clusters[edge_cluster]["Edges"].push(copied_edge); - } - } + nodes.forEach((n) => { + const node_to_update = self.get_node_by_id(n.id); + node_to_update.lcc = n.lcc ? n.lcc : helpers.HIVTRACE_UNDEFINED; }); + }; - const cluster_id_match = - self.precomputed_subclusters && - self.subcluster_threshold in self.precomputed_subclusters - ? self.precomputed_subclusters - : null; + var worker_obj = {}; + worker_obj["Nodes"] = self.nodes; + worker_obj["Edges"] = self.edges; + worker.postMessage(worker_obj); + }); - _.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 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 + ); - //var cluster_nodes = _extract_single_cluster (cluster.children, null, true); + self.compute_global_clustering_coefficients = _.once(() => { + self.compute_adjacency_list(); - var array_index = self.cluster_mapping[cluster_index]; + self.clusters.forEach((c) => { + _.defer((a_cluster) => { + const cluster_size = a_cluster.children.length; + if (cluster_size < 3) { + a_cluster.cc = helpers.HIVTRACE_UNDEFINED; + } else if (estimate_cubic_compute_cost(a_cluster, true) >= 5000000) { + a_cluster.cc = helpers.HIVTRACE_TOO_LARGE; + } else { + // pull out all the nodes that have this cluster id + const member_nodes = []; - self.clusters[array_index].priority_score = 0; + var triads = 0; + var triangles = 0; - var edges = []; + 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; + } + } + } + }); - /** 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 - ); + a_cluster.cc = triangles / triads; + } + }, c); + }); + }); - /** all edge sets with more than one edge */ - edges = _.filter(edges, (es) => es.length > 1); + self.mark_nodes_as_processing = function (property) { + self.nodes.forEach((n) => { + n[property] = helpers.HIVTRACE_PROCESSING; + }); + }; - /** sort subclusters by oldest node */ - _.each(subclusters, (c, i) => { - c.sort(oldest_nodes_first); - }); + 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(); + }; - subclusters.sort((c1, c2) => oldest_nodes_first(c1[0], c2[0])); + /*------------ Constructor ---------------*/ + function initial_json_load() { + var connected_links = {}; + var total = 0; + self.exclude_cluster_ids = {}; + self.has_hxb2_links = false; + self.cluster_sizes = []; + self.update_volatile_elements = (container) => { + tables.update_volatile_elements(self, container); + }; - let next_id = subclusters.length + 1; + 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"; + } - subclusters = _.map(subclusters, (c, i) => { - let subcluster_id = i + 1; + if (!("attributes" in d)) { + d.attributes = []; + } - 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(_networkSubclusterSeparator)[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 (d.attributes.indexOf("problematic") >= 0) { + self.has_hxb2_links = true; + d.hxb2_linked = true; + } + }); - /*if ((i+1) !== 0 + subcluster_id) { - console.log (self.clusters[array_index].cluster_id, i, "=>", subcluster_id, _.keys(precomputed_values)); - }*/ - } + /* add buttons and handlers */ + /* clusters first */ + self._is_new_node = function (n) { + return n.attributes.indexOf("new_node") >= 0; + }; - var label = - self.clusters[array_index].cluster_id + - _networkSubclusterSeparator + - subcluster_id; + self._extract_attributes_for_nodes = function (nodes, column_names) { + var result = [ + _.map(column_names, (c) => c.raw_attribute_key), + ]; - _.each(c, (n) => { - //if (!("subcluster_label" in n)) { - n.subcluster_label = label; - //} - n.priority_flag = 0; - }); + _.each(nodes, (n) => { + result.push( + _.map(column_names, (c) => { + if (c.raw_attribute_key === tables._networkNodeIDField) { + if (self._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; + }; - return { - children: _.clone(c), - parent_cluster: self.clusters[array_index], - cluster_id: label, - distances: helpers.describe_vector( - _.map(edges[i], (e) => e.length) - ), - }; - }); + self._extract_exportable_attributes = function (extended) { + var allowed_types = { + String: 1, + Date: 1, + Number: 1, + }; - _.each(subclusters, (c) => { - _compute_cluster_degrees(c); - }); + var return_array = []; - self.clusters[array_index].subclusters = subclusters; + 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"]; + }, + }, + ]; + } - /** now, for each subcluster, extract the recent and rapid part */ + return_array.push( + _.filter(self.json[_networkGraphAttrbuteID], (d) => d.type in allowed_types) + ); - /** 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 + return _.flatten(return_array, true); + }; - 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 - */ + self._extract_nodes_by_id = function (id) { + return _.filter(self.nodes, (n) => n.cluster.toString() === id.toString() || n.subcluster_label === id.toString()); + }; - _.each(subclusters, (sub) => { - // extract nodes based on dates + 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; - const date_filter = (n) => - self._filter_by_date(cutoff_long, date_field, start_date, n); + if (priority_group) { + cluster_nodes = clustersOfInterest.priority_groups_find_by_name(self, priority_group); + if (cluster_nodes) { + cluster_nodes = cluster_nodes.node_objects; + } else { + return; + } + } else { + cluster_nodes = self._extract_nodes_by_id(cluster_id); + } - var subcluster_json = _extract_single_cluster( - _.filter(sub.children, date_filter), - null, - true, - cluster_nodes + d3.select( + self.get_ui_element_selector_by_role("cluster_list_data_export", true) + ).on("click", (d) => { + if (self._is_CDC_executive_mode) { + alert(_networkWarnExecutiveMode); + } else { + helpers.export_csv_button( + self._extract_attributes_for_nodes(cluster_nodes, column_ids) ); + } + }); - 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 - ); + 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) + ); + }); + }); + } + }; - sub.rr_count = rr_cluster.length; + 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; - rr_cluster.sort((a, b) => b.length - a.length); + 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; + } - sub.priority_score = []; - sub.recent_nodes = []; + var cluster_id = button_clicked.data("cluster"); - const future_date = new Date(start_date.getTime() + 1e13); + 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") + ); + }); - _.each(rr_cluster, (recent_cluster) => { - var priority_nodes = _.groupBy(recent_cluster, (n) => - self._filter_by_date(cutoff_short, date_field, start_date, n) + $(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) ); - 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 view_toggle = $( + self.get_ui_element_selector_by_role( + "cluster_list_view_toggle", + true + ) + ); - var init_layout = get_initial_xy(packed); + 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); + } - 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); + 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 + ); } - }); - } - - //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; - }; + $(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"); - _.each([self.nodes, self.clusters], (list) => { - _.each(list, set_init_coords); - }); + 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" + ); - self.clusters.forEach(collapse_cluster); - } + const ps = clustersOfInterest.priority_groups_find_by_name(self, priority_list); + if (!(ps && self.priority_node_overlap)) return; - function change_spacing(delta) { - self.charge_correction *= delta; - network_layout.start(); - } + 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", + }, + ], + ]; - 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; + 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]); + }); - self.width += delta; - self.height += delta; + 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"); + }); - 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) + tables.add_a_sortable_table( + self, + modal.select( + self.get_ui_element_selector_by_role( + "overlap_list_data_table", + true + ) + ), + headers, + rows, + true, + null, + clustersOfInterest.get_editor() + ); + } ); + }; - _.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.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) + ); - self.compute_adjacency_list = _.once(() => { - self.nodes.forEach((n) => { - n.neighbors = d3.set(); - }); + const desc = modal.selectAll(".modal-desc"); - self.edges.forEach((e) => { - self.nodes[e.source].neighbors.add(e.target); - self.nodes[e.target].neighbors.add(e.source); - }); - }); + const proceed_btn = d3.select( + self.get_ui_element_selector_by_role( + "priority_set_merge_table_proceed", + true + ) + ); - self.compute_local_clustering_coefficients = _.once(() => { - self.compute_adjacency_list(); + if ( + clustersOfInterest.get_pg() && + clustersOfInterest.get_pg().length > 1 + ) { + desc.text("Select two or more clusters of interest to merge"); - 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++; - } + 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(clustersOfInterest.get_pg(), (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.
    Note that the clusters of interest being merged will not be automatically deleted" + ); + proceed_btn.attr("disabled", null); + } else { + desc.text("Select two or more clusters of interest to merge"); + proceed_btn.attr("disabled", "disabled"); } } - a_node.lcc = - (2 * counter) / neighborhood_size / (neighborhood_size - 1); - } - }, n); - }); - }); + 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"); + } - self.get_node_by_id = function (id) { - return self.nodes.filter((n) => n.id === id)[0]; - }; + proceed_btn.attr("disabled", "disabled").on("click", handle_merge); - self.compute_local_clustering_coefficients_worker = _.once(() => { - var worker = new Worker("workers/lcc.js"); + var rows = []; + _.each(clustersOfInterest.get_pg(), (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); + } + }); + }); - worker.onmessage = function (event) { - var nodes = event.data.Nodes; + rows.push([ + { + value: pg, + callback: function (self, 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("
    "), + html: true, + }, + ]); + }); - nodes.forEach((n) => { - const node_to_update = self.get_node_by_id(n.id); - node_to_update.lcc = n.lcc ? n.lcc : misc.undefined; - }); - }; + tables.add_a_sortable_table( + self, + modal.select( + self.get_ui_element_selector_by_role( + "priority_set_merge_table", + true + ) + ), + headers, + rows, + true, + null, + clustersOfInterest.get_editor() + ); + } + } + ); - var worker_obj = {}; - worker_obj["Nodes"] = self.nodes; - worker_obj["Edges"] = self.edges; - worker.postMessage(worker_obj); - }); + if (button_bar_ui) { + self._setup_cluster_list_view(); - 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 + var cluster_ui_container = d3.select( + self.get_ui_element_selector_by_role("cluster_operations_container") ); - }, - (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; + cluster_ui_container.selectAll("li").remove(); - 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; - } - } - } + var fix_handler = function (do_fix) { + _.each([self.clusters, self.nodes], (list) => { + _.each(list, (obj) => { + obj.fixed = do_fix; }); + }); + }; - a_cluster.cc = triangles / triads; + 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); } - }, 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(); - }; + 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; + }); + }; - /*------------ 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._is_new_node = function (n) { - return n.attributes.indexOf("new_node") >= 0; - }; - - 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 (self._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 = []; + var cluster_commands = [ + [ + __("clusters_main")["export_colors"], + () => { + const colorScheme = helpers.getCurrentCategoryColorMapping( + self.uniqValues, + self.colorizer + ); - if (extended) { - return_array = [ - { - raw_attribute_key: tables._networkNodeIDField, - type: "String", - label: "Node ID", - format: function () { - return "Node ID"; - }, + //TODO: If using database backend, use api instead + helpers.copyToClipboard(JSON.stringify(colorScheme)); }, - { - raw_attribute_key: "cluster", - type: "String", - label: "Which cluster the individual belongs to", - format: function () { - return __("clusters_tab")["cluster_id"]; - }, + true, + "hivtrace-export-color-scheme", + ], + [ + __("clusters_main")["expand_all"], + function () { + return self.expand_some_clusters(); }, - ]; - } - - return_array.push( - _.filter(self.json[_networkGraphAttrbuteID], (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(_networkWarnExecutiveMode); - } 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(", ") + 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) ); - }); - }); - } 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) + }, + true, + "hivtrace-expand-filtered", + ], + [ + __("clusters_main")["collapse_filtered"], + function () { + return self.collapse_some_clusters( + self.select_some_clusters((n) => n.match_filter) ); - - 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.
    Note that the clusters of interest being merged will not be automatically deleted" - ); - 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("
    "), - 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(self.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 (_networkGraphAttrbuteID in json) { - attributes = json[_networkGraphAttrbuteID]; - } 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[_networkGraphAttrbuteID], - 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 <= _maximumValuesInCategories &&*/ - !d["_hidden_"] - ) - - ); - - var valid_shapes = _.filter(valid_cats, (d) => ( - (d.discrete && d.dimension <= 7) || - (d["raw_attribute_key"] in _networkPresetShapeSchemes && - !d["_hidden_"]) - )); - - // sort values alphabetically for consistent coloring - - _.each([valid_cats, valid_shapes], (list) => { - _.each(list, self._aux_process_category_values); - }); - - const colorStopsPath = [ - _networkGraphAttrbuteID, - self.colorizer["category_id"], - "color_stops" - ]; - - const color_stops = _.get(graph_data, colorStopsPath, _networkContinuousColorStops); - - var valid_scales = _.filter( - _.map(graph_data[_networkGraphAttrbuteID], (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); - } - inject_attribute_node_value_by_id( - nd, - k, - self._parse_dates(a_date) - ); - } catch (err) { - inject_attribute_node_value_by_id( - nd, - k, - _networkMissing - ); - } - return self.attribute_node_value_by_id(nd, k); - }), - (v) => v === _networkMissing ? 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}); - + }, + 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", + ], [ - 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); + __("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 (m.empty()) { - return; - } - m.selectAll("li").remove(); + 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, + ]); + } - 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 (helpers.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 (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"] - ), - ], - ]) + 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) ); - } - - 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"); + }, + clustersOfInterest.get_editor, + "hivtrace-add-filtered-to-panel", + ]); + } - cat_menu - .selectAll("a") - .data((d) => d) - .enter() + 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") - .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 + - '' + - self.uniqs[d[1]] + - ""; - } - - return htm; - }) - .attr("style", (d, i, j) => { - if (d[1] === "heading") return "font-style: italic"; - if (j === 0) { - return " font-weight: bold;"; - } - return null; - }) + .text(item[0]) .attr("href", "#") - .on("click", (d) => { - if (d[2]) { - d[2].call(); - } + //.attr("id", item[3]) + .on("click", function (e) { + handler_callback(this); + //d3.event.stopPropagation(); + //d3.event.preventDefault(); }); - }); - - [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"); + 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); - cat_menu - .selectAll("a") - .data((d) => d) - .enter() - .append("a") - .html((d, i, j) => { - let htm = d[0]; - let type = "unknown"; + var button_group = d3.select( + self.get_ui_element_selector_by_role("button_group") + ); - if (_.contains(_.keys(self.schema), d[1])) { - type = self.schema[d[1]].type; - } + 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 (_.contains(_.keys(self.uniqs), d[1]) && type === "String") { - htm = - htm + - '' + - self.uniqs[d[1]] + - ""; - } + 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); + } - 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(); - } - }); - } + var export_image = d3.select( + self.get_ui_element_selector_by_role("export_image") ); - $(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"); - }); + if (!export_image.empty()) { + export_image.selectAll("div").remove(); - $(self.get_ui_element_selector_by_role("attributes_invert")) - .off("click") - .on("click", function (e) { - if (self.colorizer["category_id"]) { - graph_data[_networkGraphAttrbuteID][ - self.colorizer["category_id"] - ]["scale"].range( - graph_data[_networkGraphAttrbuteID][ - 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"); - }); + const buttonGroupDropdown = export_image + .insert("div", ":first-child") + .classed("input-group-btn dropdown-img", true); - [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"] - ), - ], - ]) - ) - ); + const dropdownList = buttonGroupDropdown + .append("ul") + .classed("dropdown-menu", true) + .attr("aria-labelledby", "dropdownImg"); - 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(); - } - }); - } - ); - } - }; + 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"); + }); - self._aux_populated_predefined_attribute = function (computed, key) { - if (_.isFunction(computed)) { - computed = computed(self); - } + 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"); + }); - if ( - !computed["depends"] || - _.every(computed["depends"], (d) => - _.has(graph_data[_networkGraphAttrbuteID], d) - ) - ) { - var extension = {}; - extension[key] = computed; - _.extend(graph_data[_networkGraphAttrbuteID], extension); - self.inject_attribute_description(key, computed); - _.each(graph_data.Nodes, (node) => { - inject_attribute_node_value_by_id( - node, - key, - computed["map"](node, self) - ); - }); + 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"); - // add unique values - if (computed.enum) { - self.uniqValues[key] = computed.enum; - } else { - self.uniqValues[key] = _.uniq( - _.map(graph_data.Nodes, (n) => - self.attribute_node_value_by_id(n, key, computed.Type === "Number") - ) - ); - } + imgBtn.append("i").classed("fa fa-image", true); - if (computed["overwrites"]) { - if ( - _.has(graph_data[_networkGraphAttrbuteID], computed["overwrites"]) - ) { - graph_data[_networkGraphAttrbuteID][computed["overwrites"]][ - "_hidden_" - ] = true; - } + imgBtn.append("span").classed("caret", true); } } - }; - if (attributes) { - /* - map attributes into nodes and into the graph object itself using - _networkGraphAttrbuteID as the key - */ + $(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) + ); - if ("attribute_map" in attributes) { - var attribute_map = attributes["attribute_map"]; + $(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) + ); - if ("map" in attribute_map && attribute_map["map"].length > 0) { - graph_data[_networkGraphAttrbuteID] = attribute_map["map"].map( - (a, i) => ({ - label: a, - type: null, - values: {}, - index: i, - range: 0, - }) - ); + $(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; + } - graph_data.Nodes.forEach((n) => { - n[_networkGraphAttrbuteID] = n.id.split(attribute_map["delimiter"]); - n[_networkGraphAttrbuteID].forEach((v, i) => { - if (i < graph_data[_networkGraphAttrbuteID].length) { - if (!(v in graph_data[_networkGraphAttrbuteID][i]["values"])) { - graph_data[_networkGraphAttrbuteID][i]["values"][v] = - graph_data[_networkGraphAttrbuteID][i]["range"]; - graph_data[_networkGraphAttrbuteID][i]["range"] += 1; - } - } - //graph_data [_networkGraphAttrbuteID][i]["values"][v] = 1 + (graph_data [_networkGraphAttrbuteID][i]["values"][v] ? graph_data [_networkGraphAttrbuteID][i]["values"][v] : 0); - }); - }); + self.update(false); + }, 250) + ); - graph_data[_networkGraphAttrbuteID].forEach((d) => { - if ( - d["range"] < graph_data.Nodes.length && - d["range"] > 1 && - d["range"] <= 20 - ) { - d["type"] = "category"; - } - }); - } - } + $(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) + ); - _.each( - self._networkPredefinedAttributeTransforms, - self._aux_populated_predefined_attribute - ); - self._aux_populate_category_menus(); + $(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) + ); + } - // populate the UI elements + if (_networkGraphAttrbuteID in json) { + attributes = json[_networkGraphAttrbuteID]; + } else if (attributes && "hivtrace" in attributes) { + attributes = attributes["hivtrace"]; } - 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]); + // Initialize class attributes + singletons = graph_data.Nodes.filter((v, i) => v.cluster === null).length; - for (var k = 0; k < sorted_array.length - max_points_to_render; k++) { - self.exclude_cluster_ids[sorted_array[k][1]] = 1; + 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.warning_string += - (self.warning_string.length ? "
    " : "") + - "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 = graph_data.Edges.filter((v, i) => v.source in connected_links && v.target in connected_links); - self.edges.forEach((e, i) => { - self.clusters[ - self.cluster_mapping[self.nodes[e.target].cluster] - ].distances.push(e.length); + 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) => { - d.distances = helpers.describe_vector(d.distances); + self.cluster_mapping[d.cluster_id] = i; + d.hxb2_linked = d.children.some((c) => c.hxb2_linked); + _compute_cluster_degrees(d); + 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]; + try { + if (options && options["extra_menu"]) { + var extra_ui_container = d3.select( + self.get_ui_element_selector_by_role("extra_operations_container") + ); - if (_is_subcluster) { - //console.log (payload); + 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(); - //this_cell.append("i") - // .classed("fa fa-arrow-circle-o-right", true).style("padding-right", "0.25em"); + 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); - /*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"]); + d3.select( + self.get_ui_element_selector_by_role("extra_operations_enclosure") + ).style("display", null); + } + } catch (err) { + console.log(err); } - 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; - - /*if (moved) { - if (d > 0) { - return "" + moved + " nodes moved +" + d + " new"; - } else { - if (d === 0) { - return "" + moved + " nodes moved"; - } else { - return "" + moved + " nodes moved " + (-d) + " removed"; - } - } + self._aux_populate_category_menus = function () { + if (button_bar_ui) { + // decide if the variable can be considered categorical by examining its range - } else { - if (d > 0) { - return "+" + d + " nodes"; - } else { - if (d === 0) { - return "no size change"; - } else { - return "" + (-d) + " nodes removed"; - } - } - }*/ + //console.log ("self._aux_populate_category_menus"); + var valid_cats = _.filter( + _.map( + graph_data[_networkGraphAttrbuteID], + 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 <= _maximumValuesInCategories &&*/ + !d["_hidden_"] + ) - 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 valid_shapes = _.filter(valid_cats, (d) => ( + (d.discrete && d.dimension <= 7) || + (d["raw_attribute_key"] in _networkPresetShapeSchemes && + !d["_hidden_"]) + )); - var labels = []; + // sort values alphabetically for consistent coloring + + _.each([valid_cats, valid_shapes], (list) => { + _.each(list, self._aux_process_category_values); + }); - 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"], - ]); - } - } + const colorStopsPath = [ + _networkGraphAttrbuteID, + self.colorizer["category_id"], + "color_stops" + ]; - 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), + const color_stops = _.get(graph_data, colorStopsPath, _networkContinuousColorStops); - var shown_types = { Existing: 1, "Newly added": 1 }, - link_class = ["Existing", "Newly added"]; + var valid_scales = _.filter( + _.map(graph_data[_networkGraphAttrbuteID], (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))]++; + }); - 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) && self._is_new_node(e.source)) - return "Newly added"; - if (_.isObject(e.target) && self._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]); - } - }); - } + var mean = values.length / color_stops; + var vrnc = _.reduce(bins, (p, c) => p + (c - mean) * (c - mean)); - function _extract_single_cluster( - nodes, - filter, - no_clone, - given_json, - include_extra_edges - ) { - /** - Extract the nodes and edges between them into a separate objects - @param nodes [array] the list of nodes to extract - @param filter [function, optional] (edge) -> bool filtering function for deciding which edges will be used to define clusters - @param no_clone [bool] if set to T, node objects are not shallow cloned in the return object + if (vrnc < low_var) { + low_var = vrnc; + d["scale"] = scl; + } + }); + } - @return [dict] the object representing "Nodes" and "Edges" in the extracted cluster + 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); + } + inject_attribute_node_value_by_id( + nd, + k, + self._parse_dates(a_date) + ); + } catch (err) { + inject_attribute_node_value_by_id( + nd, + k, + _networkMissing + ); + } + return self.attribute_node_value_by_id(nd, k); + }), + (v) => v === _networkMissing ? null : v + ); + // automatically determine the scale and see what spaces the values most evenly + if (values.length === 0) { + // invalid scale + return {}; + } - var cluster_json = {}; - var map_to_id = {}; + determine_scaling(d, values, [d3.time.scale()]); + } + return d; + }), + (d) => ( + (d.type === "Number" || + d.type === "Date" || + d.type === "Number-categories") && + !d["_hidden_"] + ) + ); - cluster_json.Nodes = _.map(nodes, (c, i) => { - map_to_id[c.id] = i; + const _menu_label_gen = (d) => (d["annotation"] ? "[" + d["annotation"] + "] " : "") + d["label"]; - if (no_clone) { - return c; - } - var cc = _.clone(c); - cc.cluster = 1; - return cc; - }); + //console.log (valid_scales); + //valid_cats.splice (0,0, {'label' : 'None', 'index' : -1}); - given_json = given_json || json; + [ + 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); - cluster_json.Edges = _.filter(given_json.Edges, (e) => { - if (_.isUndefined(e.source) || _.isUndefined(e.target)) { - return false; - } + if (m.empty()) { + return; + } + m.selectAll("li").remove(); - return ( - given_json.Nodes[e.source].id in map_to_id && - given_json.Nodes[e.target].id in map_to_id && - (include_extra_edges || !self.is_edge_injected(e)) - ); - }); + 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 (filter) { - cluster_json.Edges = _.filter(cluster_json.Edges, filter); - } + 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"] + ), + ], + ]) + ); + } - cluster_json.Edges = _.map(cluster_json.Edges, (e) => { - var ne = _.clone(e); - ne.source = map_to_id[given_json.Nodes[e.source].id]; - ne.target = map_to_id[given_json.Nodes[e.target].id]; - return ne; - }); + var cat_menu = m.selectAll("li").data(menu_items); - return cluster_json; - } + cat_menu + .enter() + .append("li") + .classed("disabled", (d) => d[0][1] === "heading") + .style("font-variant", (d) => d[0][1] < -1 ? "small-caps" : "normal"); - 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) - } + cat_menu + .selectAll("a") + .data((d) => d) + .enter() + .append("a") + .html((d, i, j) => { + let htm = d[0]; + let type = "unknown"; - if (payload.length === 2 && payload[1] >= 1) { - labels.push([ - "view cluster", - function () { - self.open_exclusive_tab_view(payload[1]); - }, - ]); - } + if (_.contains(_.keys(self.schema), d[1])) { + type = self.schema[d[1]].type; + } - 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); - } - }); - } + if (_.contains(_.keys(self.uniqs), d[1]) && type === "String") { + htm = + htm + + '' + + self.uniqs[d[1]] + + ""; + } - /*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); - }); - }; + 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(); + } + }); + }); - 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); - } - }; + [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"] + ), + ], + ]) + ) + ); - self.draw_extended_node_table = function ( - node_list, - container, - extra_columns - ) { - container = container || nodesTab.getNodeTable(); + cat_menu + .enter() + .append("li") + .style("font-variant", (d) => d[0][1] < -1 ? "small-caps" : "normal"); - if (container) { - node_list = node_list || self.nodes; - var column_ids = self._extract_exportable_attributes(true); + cat_menu + .selectAll("a") + .data((d) => d) + .enter() + .append("a") + .html((d, i, j) => { + let htm = d[0]; + let type = "unknown"; - 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 (_.contains(_.keys(self.schema), d[1])) { + type = self.schema[d[1]].type; + } - if (n) { - return n; - } - return column_ids[i]; - } - return n; - }), - (c) => c - ); + if (_.contains(_.keys(self.uniqs), d[1]) && type === "String") { + htm = + htm + + '' + + self.uniqs[d[1]] + + ""; + } - 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); - var menu_id = "hivtrace_node_column_" + payload; - var dropdown_button = dropdown - .append("button") - .classed({ - btn: true, - "btn-default": true, - "btn-xs": true, - "dropdown-toggle": true, + return htm; }) - .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"; + .attr("style", (d, i, j) => { + if (j === 0) { + return " font-weight: bold;"; } - if (word.toLowerCase() === "id") { - return "ID"; + return null; + }) + .attr("href", "#") + .on("click", (d) => { + if (d[2]) { + d[2].call(); } - - 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); + $(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"); + }); - 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 + $(self.get_ui_element_selector_by_role("attributes_invert")) + .off("click") + .on("click", function (e) { + if (self.colorizer["category_id"]) { + graph_data[_networkGraphAttrbuteID][ + self.colorizer["category_id"] + ]["scale"].range( + graph_data[_networkGraphAttrbuteID][ + self.colorizer["category_id"] + ]["scale"] + .range() + .reverse() + ); + self.clusters.forEach((the_cluster) => { + the_cluster["gradient"] = compute_cluster_gradient( + the_cluster, + self.colorizer["category_id"] ); }); - }); - 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 === _networkMissing) { - return v; - } - return _defaultDateViewFormatSlider(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)); + self.update(true); + self.draw_attribute_labels(); } + $(this).toggleClass("btn-active btn-default"); }); - } - - return this_row; - }); - - self.draw_node_table( - null, - null, - [table_headers], - table_rows, - container, - 'Showing --/-- 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) => - _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 = _defaultDateViewFormatSlider.parse(DT); - const event_date_m3y = _defaultDateViewFormatSlider.parse(DT); - event_date_m3y.setFullYear(event_date.getFullYear() - 3); - const event_date_m1y = _defaultDateViewFormatSlider.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 - ); + [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"] + ), + ], + ]) + ) + ); - 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 + 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(); + } + }); + } ); - }); + } + }; - nodesD2 = _.flatten(nodesD2, 1); - //console.log (nodesD2); + self._aux_populated_predefined_attribute = function (computed, key) { + if (_.isFunction(computed)) { + computed = computed(self); + } - info_by_event[DT] = { - connected_componets: _.map(nodesD2, (nd) => nd.length), - priority_nodes: _.map(nodesD2, (nd) => - _.map(_.filter(nd, n_filter1), (n) => n.id) - ), - }; + if ( + !computed["depends"] || + _.every(computed["depends"], (d) => + _.has(graph_data[_networkGraphAttrbuteID], d) + ) + ) { + var extension = {}; + extension[key] = computed; + _.extend(graph_data[_networkGraphAttrbuteID], extension); + self.inject_attribute_description(key, computed); + _.each(graph_data.Nodes, (node) => { + inject_attribute_node_value_by_id( + node, + key, + computed["map"](node, self) + ); + }); - info_by_event[DT]["national_priority"] = _.map( - info_by_event[DT].priority_nodes, - (m) => m.length >= self.CDC_data["autocreate-priority-set-size"] - ); - }); + // add unique values + if (computed.enum) { + self.uniqValues[key] = computed.enum; + } else { + self.uniqValues[key] = _.uniq( + _.map(graph_data.Nodes, (n) => + self.attribute_node_value_by_id(n, key, computed.Type === "Number") + ) + ); + } - const report = { - node_info: _.map(ref_set.node_objects, (n) => [ - n.id, - _defaultDateViewFormatSlider( - self.attribute_node_value_by_id(n, timeDateUtil._networkCDCDateField) - ), - ]), - event_info: info_by_event, + if (computed["overwrites"]) { + if ( + _.has(graph_data[_networkGraphAttrbuteID], computed["overwrites"]) + ) { + graph_data[_networkGraphAttrbuteID][computed["overwrites"]][ + "_hidden_" + ] = true; + } + } + } }; - /*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; - }; + if (attributes) { + /* + map attributes into nodes and into the graph object itself using + _networkGraphAttrbuteID as the key + */ - self.draw_node_table = function ( - extra_columns, - node_list, - headers, - rows, - container, - table_caption - ) { - container = container || nodesTab.getNodeTable(); + if ("attribute_map" in attributes) { + var attribute_map = attributes["attribute_map"]; - if (container) { - node_list = node_list || self.nodes; + if ("map" in attribute_map && attribute_map["map"].length > 0) { + graph_data[_networkGraphAttrbuteID] = attribute_map["map"].map( + (a, i) => ({ + label: a, + type: null, + values: {}, + index: i, + range: 0, + }) + ); - 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", - }, - ], - ]; + graph_data.Nodes.forEach((n) => { + n[_networkGraphAttrbuteID] = n.id.split(attribute_map["delimiter"]); + n[_networkGraphAttrbuteID].forEach((v, i) => { + if (i < graph_data[_networkGraphAttrbuteID].length) { + if (!(v in graph_data[_networkGraphAttrbuteID][i]["values"])) { + graph_data[_networkGraphAttrbuteID][i]["values"][v] = + graph_data[_networkGraphAttrbuteID][i]["range"]; + graph_data[_networkGraphAttrbuteID][i]["range"] += 1; + } + } + //graph_data [_networkGraphAttrbuteID][i]["values"][v] = 1 + (graph_data [_networkGraphAttrbuteID][i]["values"][v] ? graph_data [_networkGraphAttrbuteID][i]["values"][v] : 0); + }); + }); - if (extra_columns) { - _.each(extra_columns, (d) => { - if (d.prepend) { - headers[0].splice(0, 0, d.description); - } else { - headers[0].push(d.description); + graph_data[_networkGraphAttrbuteID].forEach((d) => { + if ( + d["range"] < graph_data.Nodes.length && + d["range"] > 1 && + d["range"] <= 20 + ) { + d["type"] = "category"; } }); } - - 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 + _.each( + self._networkPredefinedAttributeTransforms, + self._aux_populated_predefined_attribute ); - } - }; - - self.draw_cluster_table = function (extra_columns, element, options) { - var skip_clusters = options && options["no-clusters"]; - var skip_subclusters = !(options && options["subclusters"]); + self._aux_populate_category_menus(); - element = element || self.cluster_table; + // populate the UI elements + } - if (element) { - var headers = [ - [ - { - value: __("general")["cluster"] + " ID", - sort: function (c) { - return _.map( - c.value[0].split(_networkSubclusterSeparator), - (ss) => _networkDotFormatPadder(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_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]); - if (self.cluster_attributes) { - headers[0][1]["presort"] = "desc"; + for (var k = 0; k < sorted_array.length - max_points_to_render; k++) { + self.exclude_cluster_ids[sorted_array[k][1]] = 1; } - 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", - }); + self.warning_string += + (self.warning_string.length ? "
    " : "") + + "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."; + } - 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", - }); - } + self.edges.forEach((e, i) => { + self.clusters[ + self.cluster_mapping[self.nodes[e.target].cluster] + ].distances.push(e.length); + }); - if (!self._is_CDC_) { - headers[0].push({ - value: - __("statistics")["links_per_node"] + - "
    " + - __("statistics")["mean"] + - "[" + - __("statistics")["median"] + - ", IQR]", - html: true, - }); + self.clusters.forEach((d, i) => { + d.distances = helpers.describe_vector(d.distances); + }); + //self.clusters - headers[0].push({ - value: - __("statistics")["genetic_distances_among_linked_nodes"] + - "
    " + - __("statistics")["mean"] + - "[" + - __("statistics")["median"] + - ", IQR]", - help: "Genetic distance among nodes in the cluster", - html: true, - }); - } + self.update(); + } - if (extra_columns) { - _.each(extra_columns, (d) => { - headers[0].push(d.description); - }); - } + self._extract_single_cluster = function ( + nodes, + filter, + no_clone, + given_json, + include_extra_edges + ) { + /** + Extract the nodes and edges between them into a separate objects + @param nodes [array] the list of nodes to extract + @param filter [function, optional] (edge) -> bool filtering function for deciding which edges will be used to define clusters + @param no_clone [bool] if set to T, node objects are not shallow cloned in the return object - if (options && options["headers"]) { - options["headers"](headers); - } + @return [dict] the object representing "Nodes" and "Edges" in the extracted cluster - 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, - }, - ]; + var cluster_json = {}; + var map_to_id = {}; - 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", - }, - ]; - }; - } + cluster_json.Nodes = _.map(nodes, (c, i) => { + map_to_id[c.id] = i; - 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; - }, - }); + if (no_clone) { + return c; + } + var cc = _.clone(c); + cc.cluster = 1; + return cc; + }); - 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); - }, - }); - } + given_json = given_json || json; - if (!self._is_CDC_) { - this_row.push({ - value: d.degrees, - format: function (d) { - try { - return ( - _defaultFloatFormat(d["mean"]) + - " [" + - _defaultFloatFormat(d["median"]) + - ", " + - _defaultFloatFormat(d["Q1"]) + - " - " + - _defaultFloatFormat(d["Q3"]) + - "]" - ); - } catch (e) { - return ""; - } - }, - }); - this_row.push({ - value: d.distances, - format: function (d) { - try { - return ( - _defaultFloatFormat(d["mean"]) + - " [" + - _defaultFloatFormat(d["median"]) + - ", " + - _defaultFloatFormat(d["Q1"]) + - " - " + - _defaultFloatFormat(d["Q3"]) + - "]" - ); - } catch (e) { - return ""; - } - }, - }); - } - if (extra_columns) { - _.each(extra_columns, (ed) => { - this_row.push(ed.generator(d, self)); - }); - } + cluster_json.Edges = _.filter(given_json.Edges, (e) => { + if (_.isUndefined(e.source) || _.isUndefined(e.target)) { + return false; + } - return this_row; - }; + return ( + given_json.Nodes[e.source].id in map_to_id && + given_json.Nodes[e.target].id in map_to_id && + (include_extra_edges || !self.is_edge_injected(e)) + ); + }); - if (!skip_clusters) { - rows.push(make_row(cluster, false)); - } + if (filter) { + cluster_json.Edges = _.filter(cluster_json.Edges, filter); + } - if (!skip_subclusters) { - _.each(cluster.subclusters, (sub_cluster) => { - rows.push(make_row(sub_cluster, true)); - }); - } - }); + cluster_json.Edges = _.map(cluster_json.Edges, (e) => { + var ne = _.clone(e); + ne.source = map_to_id[given_json.Nodes[e.source].id]; + ne.target = map_to_id[given_json.Nodes[e.target].id]; + return ne; + }); - tables.add_a_sortable_table( - element, - headers, - rows, - true, - options && options["caption"] ? options["caption"] : null, - clustersOfInterest.get_editor() - ); - } - }; + return cluster_json; + } /*------------ Update layout code ---------------*/ function update_network_string(node_count, edge_count) { @@ -6644,7 +4705,7 @@ var hivtrace_cluster_network_graph = function ( // const nodes_removed = graph_data.Nodes.length - singletons - self.nodes.length; // const networkString = "Displaying a network on " + self.nodes.length + " nodes, " + self.clusters.length + " clusters" // + (clusters_removed > 0 ? " (an additional " + clusters_removed + " clusters and " + nodes_removed + " nodes have been removed due to network size constraints)" : "") + ". " - // + clusters_shown +" clusters are expanded. Of " + self.edges.length + " edges, " + draw_me.edges.length + ", and of " + self.nodes.length + " nodes, " + draw_me.nodes.length + " are displayed. "; + // + clusters_shown +" clusters are expanded. Of " + self.edges.length + " edges, " + self.draw_me.edges.length + ", and of " + self.nodes.length + " nodes, " + self.draw_me.nodes.length + " are displayed. "; // if (singletons > 0) { // networkString += "" +singletons + " singleton nodes are not shown. "; // } @@ -6694,7 +4755,7 @@ var hivtrace_cluster_network_graph = function ( container .selectAll("path") - .attr("d", misc.symbol(symbol_type).size(node_size(node))) + .attr("d", svgPlots.generate_svg_symbol(symbol_type).size(node_size(node))) .style("fill", (d) => node_color(d)); if (node.show_label) { @@ -6721,7 +4782,7 @@ var hivtrace_cluster_network_graph = function ( } container - //.attr("d", misc.symbol(symbol_type).size(node_size(node))) + //.attr("d", svgPlots.generate_svg_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") @@ -7228,7 +5289,7 @@ var hivtrace_cluster_network_graph = function ( .attr("transform", "translate(0," + offset + ")") .append("path") .attr("transform", "translate(5,-5)") - .attr("d", misc.symbol(shape_mapper(value)).size(128)) + .attr("d", svgPlots.generate_svg_symbol(shape_mapper(value)).size(128)) .classed("legend", true) .style("fill", "none"); @@ -7779,7 +5840,7 @@ var hivtrace_cluster_network_graph = function ( self.nodes.forEach((n) => { var node_T = self.attribute_node_value_by_id( n, - timeDateUtil.getClusterTimeScale() + helpers.getClusterTimeScale() ); n.date_filter = _.some(conditions[2], (d) => node_T >= d[0] && node_T <= d[1]); }); @@ -7931,14 +5992,14 @@ var hivtrace_cluster_network_graph = function ( var rendered_nodes, rendered_clusters, link; if (!soft) { - var draw_me = prepare_data_to_graph(); + self.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); + network_layout.nodes(self.draw_me.all).links(self.draw_me.edges); + update_network_string(self.draw_me.nodes.length, self.draw_me.edges.length); var edge_set = {}; - _.each(draw_me.edges, (d) => { + _.each(self.draw_me.edges, (d) => { d.pull = 0.0; var tag; @@ -7965,7 +6026,7 @@ var hivtrace_cluster_network_graph = function ( link = self.network_svg .selectAll(".link") - .data(draw_me.edges, (d) => d.id); + .data(self.draw_me.edges, (d) => d.id); //link.enter().append("line").classed("link", true); link.enter().append("path").classed("link", true); @@ -7992,7 +6053,7 @@ var hivtrace_cluster_network_graph = function ( rendered_nodes = self.network_svg .selectAll(".node") - .data(draw_me.nodes, (d) => d.id); + .data(self.draw_me.nodes, (d) => d.id); rendered_nodes.exit().remove(); @@ -8003,7 +6064,7 @@ var hivtrace_cluster_network_graph = function ( rendered_nodes.enter().append("g").append("path"); rendered_clusters = self.network_svg.selectAll(".cluster-group").data( - draw_me.clusters.map((d) => d), + self.draw_me.clusters.map((d) => d), (d) => d.cluster_id ); @@ -8018,7 +6079,8 @@ var hivtrace_cluster_network_graph = function ( .on("mouseout", cluster_pop_off) .call(network_layout.drag().on("dragstart", cluster_pop_off)); - self.draw_cluster_table( + tables.draw_cluster_table( + self, self.extra_cluster_table_columns, self.cluster_table ); @@ -8032,11 +6094,11 @@ var hivtrace_cluster_network_graph = function ( ) ) { // compute priority clusters - self.annotate_priority_clusters(timeDateUtil._networkCDCDateField, 36, 12); + self.annotate_priority_clusters(helpers._networkCDCDateField, 36, 12); try { if (self.isPrimaryGraph) { - self.priority_groups_compute_node_membership(); + clustersOfInterest.priority_groups_compute_node_membership(self); } } catch (err) { console.log(err); @@ -8137,7 +6199,8 @@ var hivtrace_cluster_network_graph = function ( // draw subcluster tables - self.draw_cluster_table( + tables.draw_cluster_table( + self, self.extra_subcluster_table_columns, self.subcluster_table, { @@ -8152,9 +6215,9 @@ var hivtrace_cluster_network_graph = function ( ); } if (self._is_CDC_) { - self.draw_extended_node_table(); + tables.draw_extended_node_table(self); } else { - self.draw_node_table(self.extra_node_table_columns); + tables.draw_node_table(self, self.extra_node_table_columns); } } else { rendered_nodes = self.network_svg.selectAll(".node"); @@ -8248,20 +6311,6 @@ var hivtrace_cluster_network_graph = 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) { @@ -8309,13 +6358,6 @@ var hivtrace_cluster_network_graph = function ( } } - self.has_network_attribute = function (key) { - if (_networkGraphAttrbuteID in self.json) { - return key in self.json[_networkGraphAttrbuteID]; - } - return false; - }; - self.inject_attribute_description = function (key, d) { if (_networkGraphAttrbuteID in self.json) { var new_attr = {}; @@ -8384,7 +6426,7 @@ var hivtrace_cluster_network_graph = function ( "Degree " + n.degree + "
    Clustering coefficient " + - misc.format_value(n.lcc, _defaultFloatFormat) + + helpers.format_value(n.lcc, _defaultFloatFormat) + ""; } else { str = "# links " + n.degree + ""; @@ -8495,7 +6537,7 @@ var hivtrace_cluster_network_graph = function ( } } - function collapse_cluster(x, keep_in_q) { + self.collapse_cluster = function (x, keep_in_q) { self.needs_an_update = true; x.collapsed = true; currently_displayed_objects -= self.cluster_sizes[x.cluster_id - 1] - 1; @@ -8509,7 +6551,7 @@ var hivtrace_cluster_network_graph = function ( return x.children.length; } - function expand_cluster(x, copy_coord) { + self.expand_cluster = function (x, copy_coord) { self.needs_an_update = true; x.collapsed = false; currently_displayed_objects += self.cluster_sizes[x.cluster_id - 1] - 1; @@ -8704,7 +6746,7 @@ var hivtrace_cluster_network_graph = function ( } function attribute_pairwise_distribution(id, dim, the_map, only_expanded) { - var scan_from = only_expanded ? draw_me.edges : self.edges; + var scan_from = only_expanded ? self.draw_me.edges : self.edges; var the_matrix = []; for (var i = 0; i < dim; i += 1) { the_matrix.push([]); @@ -8942,7 +6984,7 @@ var hivtrace_cluster_network_graph = function ( the_cluster.degrees["max"] + "" + "
    Clustering coefficient " + - misc.format_value(the_cluster.cc, _defaultFloatFormat) + + helpers.format_value(the_cluster.cc, _defaultFloatFormat) + ""; } @@ -8984,7 +7026,7 @@ var hivtrace_cluster_network_graph = function ( var cluster = self.clusters[self.cluster_mapping[open_cluster_queue[k]]]; leftover -= cluster.children.length - 1; - collapse_cluster(cluster, true); + self.collapse_cluster(cluster, true); } if (k || open_cluster_queue.length) { open_cluster_queue.splice(0, k); @@ -8992,7 +7034,7 @@ var hivtrace_cluster_network_graph = function ( } if (leftover <= 0) { - expand_cluster(d, !move_out); + self.expand_cluster(d, !move_out); } } @@ -9006,7 +7048,7 @@ var hivtrace_cluster_network_graph = function ( function show_sequences_in_cluster(d) { var sequences = {}; _.each( - _extract_single_cluster( + self._extract_single_cluster( self.clusters[self.cluster_mapping[d.cluster]].children, null, true @@ -9034,7 +7076,7 @@ var hivtrace_cluster_network_graph = function ( } function collapse_cluster_handler(d, do_update) { - collapse_cluster(self.clusters[self.cluster_mapping[d.cluster]]); + self.collapse_cluster(self.clusters[self.cluster_mapping[d.cluster]]); if (do_update) { self.update(false, 0.4); } @@ -9097,7 +7139,7 @@ var hivtrace_cluster_network_graph = function ( self.collapse_some_clusters = function (subset) { subset = subset || self.clusters; subset.forEach((x) => { - if (!x.collapsed) collapse_cluster(x); + if (!x.collapsed) self.collapse_cluster(x); }); self.update(); }; @@ -9126,7 +7168,7 @@ var hivtrace_cluster_network_graph = function ( if (self.using_time_filter) { self.using_time_filter = null; } else { - self.using_time_filter = timeDateUtil.getCurrentDate(); + self.using_time_filter = helpers.getCurrentDate(); self.using_time_filter.setFullYear( self.using_time_filter.getFullYear() - 1 ); @@ -9170,7 +7212,7 @@ var hivtrace_cluster_network_graph = function ( return { "edge-styler": function (element, d, network) { - var e_type = misc.edge_typer( + var e_type = helpers.get_edge_type( d, network.edge_types, network.edge_cluster_threshold @@ -9179,7 +7221,7 @@ var hivtrace_cluster_network_graph = function ( d3.select(element).style( "stroke", network._edge_colorizer( - misc.edge_typer(d, network.edge_types, network.edge_cluster_threshold) + helpers.get_edge_type(d, network.edge_types, network.edge_cluster_threshold) ) ); //.style ("stroke-dasharray", network._edge_dasher (d["edge_type"])); } @@ -9278,21 +7320,21 @@ var hivtrace_cluster_network_graph = function ( self._social_view_options = function ( labeled_links, shown_types, - edge_typer + get_edge_type ) { - edge_typer = - edge_typer || + get_edge_type = + get_edge_type || function (e) { return _.has(e, "edge_type") ? e["edge_type"] : ""; }; return { "edge-styler": function (element, d, network) { - var e_type = misc.edge_typer(d); + var e_type = helpers.get_edge_type(d); if (e_type !== "") { d3.select(element).style( "stroke", - network._edge_colorizer(misc.edge_typer(d)) + network._edge_colorizer(helpers.get_edge_type(d)) ); //.style ("stroke-dasharray", network._edge_dasher (d["edge_type"])); d.is_hidden = !network.shown_types[e_type]; @@ -9533,9 +7575,9 @@ var hivtrace_cluster_network_graph = function ( self.update_clusters_with_injected_nodes(null, null, annotation); if (self._is_CDC_) { - self.draw_extended_node_table(self.json.Nodes); + tables.draw_extended_node_table(self, self.json.Nodes); } else { - self.draw_node_table(self.extra_node_table_columns, self.json.Nodes); + tables.draw_node_table(self, self.extra_node_table_columns, self.json.Nodes); } if (!self.extra_cluster_table_columns) { self.extra_cluster_table_columns = []; @@ -9648,9 +7690,9 @@ var hivtrace_cluster_network_graph = function ( edge_filter_for_subclusters, true ); - //cv.annotate_priority_clusters(timeDateUtil._networkCDCDateField, 36, 12); + //cv.annotate_priority_clusters(helpers._networkCDCDateField, 36, 12); //cv.handle_attribute_categorical("recent_rapid"); - cv._refresh_subcluster_view(self.today || timeDateUtil.getCurrentDate()); + cv._refresh_subcluster_view(self.today || helpers.getCurrentDate()); }; var injected_column_subcluster = [ @@ -9731,7 +7773,7 @@ var hivtrace_cluster_network_graph = function ( cluster.cluster_id, ], - callback: function (element, payload) { + callback: function (self, element, payload) { var this_cell = d3.select(element); this_cell.text(Number(payload[0]) + " " + annotation + " nodes. "); var other_clusters = []; @@ -9837,11 +7879,14 @@ var hivtrace_cluster_network_graph = function ( if (self.extra_cluster_table_columns) { self.extra_cluster_table_columns = self.extra_cluster_table_columns.concat(injected_column); + + console.log("HIT") } else { self.extra_cluster_table_columns = injected_column; } - self.draw_cluster_table( + tables.draw_cluster_table( + self, self.extra_cluster_table_columns, self.cluster_table ); @@ -9855,7 +7900,8 @@ var hivtrace_cluster_network_graph = function ( } else { self.extra_subcluster_table_columns = injected_column_subcluster; } - self.draw_cluster_table( + tables.draw_cluster_table( + self, self.extra_subcluster_table_columns, self.subcluster_table, { subclusters: true, "no-clusters": true } @@ -9994,7 +8040,6 @@ var hivtrace_cluster_network_graph = function ( var network_layout = d3.layout .force() - .on("tick", tick) .charge((d) => { if (self.showing_on_map) { return -60; @@ -10116,7 +8161,7 @@ var hivtrace_cluster_network_graph = function ( if (options["priority-sets-url"]) { const is_writeable = options["is-writeable"]; - self.load_priority_sets(options["priority-sets-url"], is_writeable); + clustersOfInterest.load_priority_sets(self, options["priority-sets-url"], is_writeable); } if (self.showing_diff) { diff --git a/src/clustersOfInterest.js b/src/clustersOI/clusterOI.js similarity index 59% rename from src/clustersOfInterest.js rename to src/clustersOI/clusterOI.js index 1fd81ef..88768fc 100644 --- a/src/clustersOfInterest.js +++ b/src/clustersOI/clusterOI.js @@ -1,23 +1,39 @@ +// TODO: put in separate `clusterOI` folder for now, should refactor later to multiple files within this folder 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 utils from "./utils.js"; -import * as clusterNetwork from "./clusternetwork.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 helpers from "../helpers.js"; +import * as clusterNetwork from "../clusternetwork.js"; +import * as tables from "../tables.js"; +import * as nodesTab from "../nodesTab.js"; +import * as svgPlots from "../svgPlots.js"; +import { hivtrace_cluster_depthwise_traversal } from "../helpers.js"; let priority_set_editor = null; +let defined_priority_groups = []; +/** + { + 'name' : 'unique name', + 'nodes' : [ + { + 'node_id' : text, + 'added' : date, + 'kind' : text + }], + 'created' : date, + 'description' : 'text', + 'modified' : date, + 'kind' : 'text' + } +*/ function init(self) { if (self._is_CDC_ && self.isPrimaryGraph) { - let new_set = utils.get_ui_element_selector_by_role("new_priority_set"); + let new_set = helpers.get_ui_element_selector_by_role("new_priority_set"); if (new_set) { window.addEventListener("beforeunload", (e) => { - if (self.priority_groups_pending() > 0) { + if (defined_priority_groups.some(pg => pg.pending)) { e.preventDefault(); return "There are cluster of interest that have not been confirmed. Closing the window now will not finalize their creation."; } @@ -26,31 +42,28 @@ function init(self) { d3.selectAll(new_set).on("click", (e) => { open_editor(self, []); - self.redraw_tables(); + redraw_tables(self); }); } - let merge_sets = utils.get_ui_element_selector_by_role("merge_priority_sets"); + let merge_sets = helpers.get_ui_element_selector_by_role("merge_priority_sets"); if (merge_sets) { d3.selectAll(merge_sets).on("click", (e) => { $( - utils.get_ui_element_selector_by_role("priority_set_merge") + helpers.get_ui_element_selector_by_role("priority_set_merge") ).modal(); }); } } } -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; +function get_editor() { + return priority_set_editor; +} + +function get_pg() { + return defined_priority_groups; } function open_editor( @@ -136,7 +149,7 @@ function open_editor( .classed("form-inline", true); var form_grp = form.append("div").classed("form-group", true); - var node_ids_selector = form_grp + form_grp .append("input") .classed("form-control input-sm", true) .attr("placeholder", "Add node by ID") @@ -302,12 +315,12 @@ function open_editor( let createdDate = existing_set && validation_mode && validation_mode.length ? existing_set.created - : timeDateUtil.getCurrentDate(); + : helpers.getCurrentDate(); let modifiedDate = validation_mode === "validate" && created_by === clusterNetwork._cdcCreatedBySystem ? self.today - : timeDateUtil.getCurrentDate(); + : helpers.getCurrentDate(); function save_priority_set() { /** @@ -331,14 +344,14 @@ function open_editor( (k) => $( d3 - .select(utils.get_ui_element_selector_by_role(k)) + .select(helpers.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) + priority_groups_check_name(defined_priority_groups, name, panel_object.prior_name) ) { let set_description = { name: name, @@ -361,7 +374,7 @@ function open_editor( }; if (tracking !== clusterNetwork._cdcTrackingNone) { - let added_nodes = self.auto_expand_pg_handler(set_description); + let added_nodes = auto_expand_pg(self, set_description); if (added_nodes.size) { if ( confirm( @@ -376,7 +389,7 @@ function open_editor( let n = self.json.Nodes[nid]; set_description.nodes.push({ name: n.id, - added: timeDateUtil.getCurrentDate(), + added: helpers.getCurrentDate(), kind: clusterNetwork._cdcPrioritySetDefaultNodeKind, }); }); @@ -404,7 +417,7 @@ function open_editor( panel_object.close(); if (validation_mode === "validate") { if (self.priority_set_table_writeable) { - let tab_pill = utils.get_ui_element_selector_by_role("priority_set_counts"), + let tab_pill = helpers.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); @@ -415,7 +428,7 @@ function open_editor( panel_object.first_save = false; } let panel_to_focus = document.querySelector( - utils.get_ui_element_selector_by_role("priority-panel-name") + helpers.get_ui_element_selector_by_role("priority-panel-name") ); if (panel_to_focus) panel_to_focus.focus(); return res; @@ -459,7 +472,7 @@ function open_editor( let current_text = $(this).val(); if ( priority_groups_check_name( - self.defined_priority_groups, + defined_priority_groups, current_text, panel_object.prior_name ) @@ -495,7 +508,7 @@ function open_editor( } var auto_object = autocomplete( - utils.get_ui_element_selector_by_role("priority-panel-nodeids"), + helpers.get_ui_element_selector_by_role("priority-panel-nodeids"), { hint: false }, [ { @@ -685,13 +698,13 @@ function open_editor( var del_form_generator = function () { return ( - `
    + `
    -`); + n.name) @@ -928,11 +941,769 @@ function open_editor( }, onclosed: function () { priority_set_editor = null; - self.redraw_tables(); + redraw_tables(self); }, }); } +function has_network_attribute(self, key) { + if (clusterNetwork._networkGraphAttrbuteID in self.json) { + return key in self.json[clusterNetwork._networkGraphAttrbuteID]; + } + return false; +}; + +function _generate_auto_id(self, subcluster_id) { + const id = + self.CDC_data["jurisdiction_code"] + + "_" + + clusterNetwork._defaultDateViewFormatClusterCreate(self.CDC_data["timestamp"]) + + "_" + + subcluster_id; + let suffix = ""; + let k = 1; + let found = self.auto_create_priority_sets.find((d) => d.name === id + suffix) + || defined_priority_groups.find((d) => d.name === id + suffix); + while (found !== undefined) { + suffix = "_" + k; + k++; + found = self.auto_create_priority_sets.find((d) => d.name === id + suffix) + || defined_priority_groups.find((d) => d.name === id + suffix); + } + return id + suffix; +} + +function load_priority_sets(self, url, is_writeable) { + d3.json(url, (error, results) => { + if (error) { + throw Error("Failed loading cluster of interest file " + error.responseURL); + } else { + let latest_date = new Date(); + latest_date.setFullYear(1900); + defined_priority_groups = _.clone(results); + _.each(defined_priority_groups, (pg) => { + _.each(pg.nodes, (n) => { + try { + n.added = clusterNetwork._defaultDateFormats[0].parse(n.added); + if (n.added > latest_date) { + latest_date = n.added; + } + } catch { + // do nothing + } + }); + }); + + self.priority_set_table_writeable = is_writeable === "writeable"; + + priority_groups_validate( + self, + defined_priority_groups, + self._is_CDC_auto_mode + ); + + self.auto_create_priority_sets = []; + // propose some + const today_string = clusterNetwork._defaultDateFormats[0](self.today); + const node_id_to_object = {}; + + _.each(self.json.Nodes, (n, i) => { + node_id_to_object[n.id] = n; + }); + + if (self._is_CDC_auto_mode) { + _.each(self.clusters, (cluster_data, cluster_id) => { + _.each(cluster_data.subclusters, (subcluster_data) => { + _.each(subcluster_data.priority_score, (priority_score, i) => { + if ( + priority_score.length >= + self.CDC_data["autocreate-priority-set-size"] + ) { + // only generate a new set if it doesn't match what is already there + const node_set = {}; + _.each(subcluster_data.recent_nodes[i], (n) => { + node_set[n] = 1; + }); + + const matched_groups = _.filter( + _.filter( + defined_priority_groups, + (pg) => + pg.kind in clusterNetwork._cdcPrioritySetKindAutoExpand && + pg.createdBy === clusterNetwork._cdcCreatedBySystem && + pg.tracking === clusterNetwork._cdcTrackingOptionsDefault + ), + (pg) => { + const matched = _.countBy( + _.map(pg.nodes, (pn) => pn.name in node_set) + ); + //if (pg.name === 'FL_201709_141.1') console.log (matched); + return ( + //matched[true] === subcluster_data.recent_nodes[i].length + matched[true] >= 1 + ); + } + ); + + if (matched_groups.length >= 1) { + return; + } + + const autoname = _generate_auto_id(self, subcluster_data.cluster_id); + self.auto_create_priority_sets.push({ + name: autoname, + description: + "Automatically created cluster of interest " + autoname, + nodes: _.map(subcluster_data.recent_nodes[i], (n) => + priority_group_node_record(n, self.today) + ), + created: today_string, + kind: clusterNetwork._cdcPrioritySetKindAutomaticCreation, + tracking: clusterNetwork._cdcTrackingOptions[0], + createdBy: clusterNetwork._cdcCreatedBySystem, + autocreated: true, + autoexpanded: false, + pending: true, + }); + } + }); + }); + }); + } + + if (self.auto_create_priority_sets.length) { + // SLKP 20200727 now check to see if any of the priority sets + // need to be auto-generated + //console.log (self.auto_create_priority_sets); + defined_priority_groups.push(...self.auto_create_priority_sets); + } + const autocreated = defined_priority_groups.filter( + (pg) => pg.autocreated + ).length, + autoexpanded = defined_priority_groups.filter( + (pg) => pg.autoexpanded + ).length, + automatic_action_taken = autocreated + autoexpanded > 0, + left_to_review = defined_priority_groups.filter( + (pg) => pg.pending + ).length; + + if (automatic_action_taken) { + self.warning_string += + "
    Automatically created " + + autocreated + + " and expanded " + + autoexpanded + + " clusters of interest." + + (left_to_review > 0 + ? " Please review clusters in the Clusters of Interest tab.
    " + : ""); + self.display_warning(self.warning_string, true); + } + + const tab_pill = helpers.get_ui_element_selector_by_role("priority_set_counts") + + if (!self.priority_set_table_writeable) { + const rationale = + is_writeable === "old" + ? "the network is older than some of the Clusters of Interest" + : "the network was ran in standalone mode so no data is stored"; + self.warning_string += `

    READ-ONLY mode for Clusters of Interest is enabled because ${rationale}. None of the changes to clustersOI made during this session will be recorded.

    `; + self.display_warning(self.warning_string, true); + if (tab_pill) { + d3.select(tab_pill).text("Read-only"); + } + } else if (tab_pill && left_to_review > 0) { + d3.select(tab_pill).text(left_to_review); + d3.select("#banner_coi_counts").text(left_to_review); + } + + priority_groups_validate(self, defined_priority_groups); + _.each(self.auto_create_priority_sets, (pg) => + priority_groups_update_node_sets(self, pg.name, "insert") + ); + const groups_that_expanded = defined_priority_groups.filter( + (pg) => pg.expanded + ); + _.each(groups_that_expanded, (pg) => + priority_groups_update_node_sets(self, pg.name, "update") + ); + + draw_priority_set_table(self); + if ( + self.showing_diff && + has_network_attribute(self, "subcluster_or_priority_node") + ) { + self.handle_attribute_categorical("subcluster_or_priority_node"); + } + //self.update(); + } + }); +}; + +function priority_group_node_record(node_id, date, kind) { + return { + name: node_id, + added: date, + kind: kind || clusterNetwork._cdcPrioritySetDefaultNodeKind, + autoadded: true, + }; +}; + +function priority_groups_compute_overlap(self, groups) { + /** + compute the overlap between priority sets (PS) + + 1. Populate self.priority_node_overlap dictionary which + stores, for every node present in AT LEAST ONE PS, the set of all + PGs it belongs to, as in "node-id" => set ("PG1", "PG2"...) + + 2. For each PS, create and populate a member field, .overlaps + which is a dictionary that stores + { + sets : #of PS with which it shares nodes + nodes: the # of nodes contained in overlaps + } + + */ + self.priority_node_overlap = {}; + const size_by_pg = {}; + _.each(groups, (pg) => { + size_by_pg[pg.name] = pg.nodes.length; + _.each(pg.nodes, (n) => { + if (!(n.name in self.priority_node_overlap)) { + self.priority_node_overlap[n.name] = new Set(); + } + self.priority_node_overlap[n.name].add(pg.name); + }); + }); + + _.each(groups, (pg) => { + const overlap = { + sets: new Set(), + nodes: 0, + supersets: [], + duplicates: [], + }; + + const by_set_count = {}; + _.each(pg.nodes, (n) => { + if (self.priority_node_overlap[n.name].size > 1) { + overlap.nodes++; + self.priority_node_overlap[n.name].forEach((pgn) => { + if (pgn !== pg.name) { + if (!(pgn in by_set_count)) { + by_set_count[pgn] = []; + } + by_set_count[pgn].push(n.name); + } + overlap.sets.add(pgn); + }); + } + }); + + _.each(by_set_count, (nodes, name) => { + if (nodes.length === pg.nodes.length) { + if (size_by_pg[name] === pg.nodes.length) { + overlap.duplicates.push(name); + } else { + overlap.supersets.push(name); + } + } + }); + + pg.overlap = { + nodes: overlap.nodes, + sets: Math.max(0, overlap.sets.size - 1), + superset: overlap.supersets, + duplicate: overlap.duplicates, + }; + }); +}; + +function priority_groups_update_node_sets(self, name, operation) { + // name : the name of the priority group being added + // operation: one of + // "insert" , "delete", "update" + + const sets = priority_groups_export().filter((pg) => pg.name === name); + const to_post = { + operation: operation, + name: name, + url: window.location.href, + sets: JSON.stringify(sets), + }; + + if (self.priority_set_table_write && self.priority_set_table_writeable) { + d3.text(self.priority_set_table_write) + .header("Content-Type", "application/json") + .post(JSON.stringify(to_post), (error, data) => { + if (error) { + console.log("received fatal error:", error); + /* + $(".container").html( + '
    FATAL ERROR. Please reload the page and contact help desk.
    ' + ); + */ + } + }); + } +}; + +function priority_groups_compute_node_membership(self) { + const pg_nodesets = []; + + _.each(defined_priority_groups, (g) => { + pg_nodesets.push([ + g.name, + g.createdBy === clusterNetwork._cdcCreatedBySystem, + new Set(_.map(g.nodes, (n) => n.name)), + ]); + }); + + const pg_enum = [ + "Yes (dx≤12 months)", + "Yes (1236 months)", + "No", + ]; + + _.each( + { + subcluster_or_priority_node: { + depends: [helpers._networkCDCDateField], + label: clusterNetwork._cdcPOImember, + enum: pg_enum, + type: "String", + volatile: true, + color_scale: function () { + return d3.scale + .ordinal() + .domain(pg_enum.concat([clusterNetwork._networkMissing])) + .range([ + "red", + "orange", + "yellow", + "steelblue", + clusterNetwork._networkMissingColor, + ]); + }, + map: function (node) { + const npcoi = _.some(pg_nodesets, (d) => d[1] && d[2].has(node.id)); + if (npcoi) { + const cutoffs = [ + helpers.getNMonthsAgo(self.get_reference_date(), 12), + helpers.getNMonthsAgo(self.get_reference_date(), 36), + ]; + + //const ysd = self.attribute_node_value_by_id( + // node, + // "years_since_dx" + //); + + if ( + self._filter_by_date( + cutoffs[0], + helpers._networkCDCDateField, + self.get_reference_date(), + node, + false + ) + ) + return pg_enum[0]; + if ( + self._filter_by_date( + cutoffs[1], + helpers._networkCDCDateField, + self.get_reference_date(), + node, + false + ) + ) + return pg_enum[1]; + return pg_enum[2]; + } + return pg_enum[3]; + }, + }, + cluster_uid: { + depends: [helpers._networkCDCDateField], + label: "Clusters of Interest", + type: "String", + volatile: true, + map: function (node) { + const memberships = _.filter(pg_nodesets, (d) => d[2].has(node.id)); + if (memberships.length === 1) { + return memberships[0][0]; + } else if (memberships.length > 1) { + return "Multiple"; + } + return "None"; + }, + }, + subcluster_id: { + depends: [helpers._networkCDCDateField], + label: "Subcluster ID", + type: "String", + //label_format: d3.format(".2f"), + map: function (node) { + if (node) { + return node.subcluster_label || "None"; + } + return clusterNetwork._networkMissing; + }, + }, + }, + self._aux_populated_predefined_attribute + ); + self._aux_populate_category_menus(); +}; + +function priority_groups_export(group_set, include_unvalidated) { + group_set = group_set || defined_priority_groups; + + return _.map( + _.filter(group_set, (g) => include_unvalidated || g.validated), + (g) => ({ + name: g.name, + description: g.description, + nodes: g.nodes, + modified: clusterNetwork._defaultDateFormats[0](g.modified), + kind: g.kind, + created: clusterNetwork._defaultDateFormats[0](g.created), + createdBy: g.createdBy, + tracking: g.tracking, + autocreated: g.autocreated, + autoexpanded: g.autoexpanded, + pending: g.pending, + }) + ); +}; + +function priority_groups_validate(self, groups, auto_extend) { + /** + groups is a list of priority groups + + name: unique string + description: string, + nodes: { + { + 'id' : node id, + 'added' : date, + 'kind' : enum (one of _cdcPrioritySetNodeKind) + } + }, + created: date, + kind: enum (one of _cdcPrioritySetKind), + tracking: enum (one of _cdcTrackingOptions) + createdBy : enum (on of [_cdcCreatedBySystem,_cdcCreatedByManual]) + */ + + if (_.some(groups, (g) => !g.validated)) { + const priority_subclusters = _.map( + _.filter( + _.flatten( + _.map( + _.flatten( + _.map(self.clusters, (c) => + _.filter( + _.filter(c.subclusters, (sc) => sc.priority_score.length) + ) + ) + ), + (d) => d.priority_score + ), + 1 + ), + (d) => d.length >= self.CDC_data["autocreate-priority-set-size"] + ), + (d) => new Set(d) + ); + + const nodeset = {}; + const nodeID2idx = {}; + _.each(self.json.Nodes, (n, i) => { + nodeset[n.id] = n; + nodeID2idx[n.id] = i; + }); + _.each(groups, (pg) => { + if (!pg.validated) { + pg.node_objects = []; + pg.not_in_network = []; + pg.validated = true; + pg.created = _.isDate(pg.created) + ? pg.created + : clusterNetwork._defaultDateFormats[0].parse(pg.created); + if (pg.modified) { + pg.modified = _.isDate(pg.modified) ? pg.modified : clusterNetwork._defaultDateFormats[0].parse(pg.modified); + } else { + pg.modified = pg.created; + } + if (!pg.tracking) { + if (pg.kind === clusterNetwork._cdcPrioritySetKind[0]) { + pg.tracking = clusterNetwork._cdcTrackingOptions[0]; + } else { + pg.tracking = clusterNetwork._cdcTrackingOptions[4]; + } + } + if (!pg.createdBy) { + if (pg.kind === clusterNetwork._cdcPrioritySetKind[0]) { + pg.createdBy = clusterNetwork._cdcCreatedBySystem; + } else { + pg.createdBy = clusterNetwork._cdcCreatedByManual; + } + } + + _.each(pg.nodes, (node) => { + const nodeid = node.name; + if (nodeid in nodeset) { + pg.node_objects.push(nodeset[nodeid]); + } else { + pg.not_in_network.push(nodeid); + } + }); + + /** extract network data at 0.015 and subcluster thresholds + filter on dates subsequent to created date + **/ + + const my_nodeset = new Set(_.map(pg.node_objects, (n) => n.id)); + + const node_set15 = _.flatten( + hivtrace_cluster_depthwise_traversal( + self.json.Nodes, + self.json.Edges, + (e) => e.length <= 0.015, + null, + pg.node_objects + ) + ); + + const saved_traversal_edges = auto_extend ? [] : null; + + const node_set_subcluster = _.flatten( + hivtrace_cluster_depthwise_traversal( + self.json.Nodes, + self.json.Edges, + (e) => e.length <= self.subcluster_threshold, + saved_traversal_edges, + pg.node_objects + ) + ); + + const direct_at_15 = new Set(); + + const json15 = self._extract_single_cluster( + node_set15, + (e) => ( + e.length <= 0.015 && + (my_nodeset.has(self.json.Nodes[e.target].id) || + my_nodeset.has(self.json.Nodes[e.source].id)) + ), + //null, + true + ); + + _.each(json15["Edges"], (e) => { + _.each([e.source, e.target], (nid) => { + if (!my_nodeset.has(json15["Nodes"][nid].id)) { + direct_at_15.add(json15["Nodes"][nid].id); + } + }); + }); + + const current_time = self.today; + + const json_subcluster = self._extract_single_cluster( + node_set_subcluster, + (e) => ( + e.length <= self.subcluster_threshold && + (my_nodeset.has(self.json.Nodes[e.target].id) || + my_nodeset.has(self.json.Nodes[e.source].id)) + /*|| (auto_extend && (self._filter_by_date( + pg.modified || pg.created, + helpers._networkCDCDateField, + current_time, + self.json.Nodes[e.target], + true + ) || self._filter_by_date( + pg.modified || pg.created, + helpers._networkCDCDateField, + current_time, + self.json.Nodes[e.source], + true + )))*/ + ), + true + ); + + const direct_subcluster = new Set(); + const direct_subcluster_new = new Set(); + _.each(json_subcluster["Edges"], (e) => { + _.each([e.source, e.target], (nid) => { + if (!my_nodeset.has(json_subcluster["Nodes"][nid].id)) { + direct_subcluster.add(json_subcluster["Nodes"][nid].id); + + if ( + self._filter_by_date( + pg.modified || pg.created, + helpers._networkCDCDateField, + current_time, + json_subcluster["Nodes"][nid], + true + ) + ) + direct_subcluster_new.add(json_subcluster["Nodes"][nid].id); + } + }); + }); + + pg.partitioned_nodes = _.map( + [ + [node_set15, direct_at_15], + [node_set_subcluster, direct_subcluster], + ], + (ns) => { + const nodesets = { + existing_direct: [], + new_direct: [], + existing_indirect: [], + new_indirect: [], + }; + + _.each(ns[0], (n) => { + if (my_nodeset.has(n.id)) return; + let key; + if ( + self._filter_by_date( + pg.modified || pg.created, + helpers._networkCDCDateField, + current_time, + n, + true + ) + ) { + key = "new"; + } else { + key = "existing"; + } + + if (ns[1].has(n.id)) { + key += "_direct"; + } else { + key += "_indirect"; + } + + nodesets[key].push(n); + }); + + return nodesets; + } + ); + + if (auto_extend && pg.tracking !== clusterNetwork._cdcTrackingNone) { + const added_nodes = auto_expand_pg(self, pg, nodeID2idx); + + if (added_nodes.size) { + _.each([...added_nodes], (nid) => { + const n = self.json.Nodes[nid]; + pg.nodes.push({ + name: n.id, + added: current_time, + kind: clusterNetwork._cdcPrioritySetDefaultNodeKind, + autoadded: true, + }); + pg.node_objects.push(n); + }); + pg.validated = false; + pg.autoexpanded = true; + pg.pending = true; + pg.expanded = added_nodes.size; + pg.modified = self.today; + } + } + + const node_set = new Set(_.map(pg.nodes, (n) => n.name)); + pg.meets_priority_def = _.some(priority_subclusters, (ps) => ( + _.filter([...ps], (psi) => node_set.has(psi)).length === ps.size + )); + const cutoff12 = helpers.getNMonthsAgo(self.get_reference_date(), 12); + pg.last12 = _.filter(pg.node_objects, (n) => + self._filter_by_date( + cutoff12, + helpers._networkCDCDateField, + self.today, + n, + false + ) + ).length; + } + }); + } +}; + +function auto_expand_pg(self, pg, nodeID2idx) { + if (!nodeID2idx) { + const nodeset = {}; + nodeID2idx = {}; + _.each(self.json.Nodes, (n, i) => { + nodeset[n.id] = n; + nodeID2idx[n.id] = i; + }); + } + + const core_node_set = new Set(_.map(pg.nodes, (n) => nodeID2idx[n.name])); + const added_nodes = new Set(); + const filter = clusterNetwork._cdcTrackingOptionsFilter[pg.tracking]; + + if (filter) { + const time_cutoff = helpers.getNMonthsAgo( + self.get_reference_date(), + clusterNetwork._cdcTrackingOptionsCutoff[pg.tracking] + ); + const expansion_test = hivtrace_cluster_depthwise_traversal( + self.json.Nodes, + self.json.Edges, + (e) => { + let pass = filter(e); + if (pass) { + if (!(core_node_set.has(e.source) && core_node_set.has(e.target))) { + pass = + pass && + self._filter_by_date( + time_cutoff, + helpers._networkCDCDateField, + self.get_reference_date(), + self.json.Nodes[e.source] + ) && + self._filter_by_date( + time_cutoff, + helpers._networkCDCDateField, + self.get_reference_date(), + self.json.Nodes[e.target] + ); + } + } + return pass; + }, + false, + _.filter( + _.map([...core_node_set], (d) => self.json.Nodes[d]), + (d) => d + ) + ); + + _.each(expansion_test, (c) => { + _.each(c, (n) => { + if (!core_node_set.has(nodeID2idx[n.id])) { + added_nodes.add(nodeID2idx[n.id]); + } + }); + }); + } + return added_nodes; +}; + function handle_inline_confirm( this_button, generator, @@ -957,10 +1728,10 @@ function handle_inline_confirm( "#" + clicked_object.attr("aria-describedby") ); var textarea_element = popover_div.selectAll( - utils.get_ui_element_selector_by_role("priority-description-form") + helpers.get_ui_element_selector_by_role("priority-description-form") ); var button_element = popover_div.selectAll( - utils.get_ui_element_selector_by_role("priority-description-save") + helpers.get_ui_element_selector_by_role("priority-description-save") ); textarea_element.text(text); if (disabled) textarea_element.attr("disabled", true); @@ -970,7 +1741,7 @@ function handle_inline_confirm( this_button.click(); }); button_element = popover_div.selectAll( - utils.get_ui_element_selector_by_role("priority-description-dismiss") + helpers.get_ui_element_selector_by_role("priority-description-dismiss") ); button_element.on("click", (d) => { d3.event.preventDefault(); @@ -1013,9 +1784,9 @@ function _action_drop_down(self, pg) { if (!self._is_CDC_executive_mode) { dropdown.push({ - label: "Clone this cluster of interest in a new editor pane", + 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 ref_set = priority_groups_find_by_name(self, pg.name); let copied_node_objects = _.clone(ref_set.node_objects); priority_set_inject_node_attibutes( self, @@ -1029,7 +1800,7 @@ function _action_drop_down(self, pg) { "Clone of " + pg.name, ref_set.kind ); - self.redraw_tables(); + redraw_tables(self); }, }); if (pg.createdBy !== "System") { @@ -1037,7 +1808,7 @@ function _action_drop_down(self, pg) { 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); + priority_groups_remove_set(self, pg.name, true); } }, }); @@ -1046,7 +1817,7 @@ function _action_drop_down(self, pg) { label: "View nodes in this cluster of interest", data: { toggle: "modal", - target: utils.get_ui_element_selector_by_role("cluster_list"), + target: helpers.get_ui_element_selector_by_role("cluster_list"), priority_set: pg.name, }, }); @@ -1054,7 +1825,7 @@ function _action_drop_down(self, pg) { dropdown.push({ label: "Modify this cluster of interest", action: function (button, value) { - let ref_set = self.priority_groups_find_by_name(pg.name); + let ref_set = priority_groups_find_by_name(self, pg.name); if (ref_set) { /*if (ref_set.modified.getTime() > self.today.getTime()) { @@ -1076,7 +1847,7 @@ function _action_drop_down(self, pg) { ref_set, ref_set.tracking ); - self.redraw_tables(); + redraw_tables(self); } }, }); @@ -1084,14 +1855,14 @@ function _action_drop_down(self, pg) { 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 ref_set = priority_groups_find_by_name(self, pg.name); + let report = generate_coi_temporal_report(self, ref_set); let container = self.open_exclusive_tab_view_aux( null, "History of " + pg.name, {} ); - misc.coi_timeseries( + svgPlots.plot_coi_timeseries( report, d3.select("#" + container).style("padding", "20px"), 1000 @@ -1105,9 +1876,9 @@ function _action_drop_down(self, pg) { 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); + priority_groups = priority_groups || defined_priority_groups; + priority_groups_compute_node_membership(self); + priority_groups_compute_overlap(self, priority_groups); var headers = [ [ { @@ -1283,7 +2054,7 @@ function draw_priority_set_table(self, container, priority_groups) { { value: [ pg.node_objects.length, - _.filter(pg.nodes, (g) => self.priority_groups_is_new_node(pg, g)) + _.filter(pg.nodes, (g) => g.autoadded) .length, pg.createdBy === clusterNetwork._cdcCreatedBySystem && pg.pending, pg.meets_priority_def, @@ -1360,7 +2131,7 @@ function draw_priority_set_table(self, container, priority_groups) { label: "List overlaps", data: { toggle: "modal", - target: utils.get_ui_element_selector_by_role("overlap_list"), + target: helpers.get_ui_element_selector_by_role("overlap_list"), priority_set: pg.name, }, }, @@ -1392,7 +2163,7 @@ function draw_priority_set_table(self, container, priority_groups) { icon: "fa-eye", help: "Review and adjust this cluster of interest", action: function (button, value) { - let nodeset = self.priority_groups_find_by_name(value); + let nodeset = priority_groups_find_by_name(self, value); if (nodeset) { if (get_editor()) { alert( @@ -1411,7 +2182,7 @@ function draw_priority_set_table(self, container, priority_groups) { pg.tracking, pg.createdBy ); - self.redraw_tables(); + redraw_tables(self); } } }, @@ -1424,6 +2195,7 @@ function draw_priority_set_table(self, container, priority_groups) { 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) { @@ -1440,7 +2212,7 @@ function draw_priority_set_table(self, container, priority_groups) { edit_form_generator, pg.description, (d) => { - self.priority_groups_edit_set_description(pg.name, d, true); + priority_groups_set_description(self, pg.name, d, true); } ); }, @@ -1455,7 +2227,7 @@ function draw_priority_set_table(self, container, priority_groups) { 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); + let nodeset = priority_groups_find_by_name(self, value); if (nodeset) { get_editor().append_node_objects( nodeset.node_objects @@ -1486,8 +2258,8 @@ function draw_priority_set_table(self, container, priority_groups) { let has_required_actions = ""; - /* let has_automatic = self.priority_groups_pending(); - let has_expanded = self.priority_groups_expanded(); + /* let has_automatic = defined_priority_groups.some(pg => pg.pending); + let has_expanded = defined_priority_groups.some(pg => pg.expanded); if (has_automatic + has_expanded) { let labeler = (c, description, c2) => { @@ -1508,6 +2280,7 @@ function draw_priority_set_table(self, container, priority_groups) { }*/ tables.add_a_sortable_table( + self, container, headers, rows, @@ -1520,35 +2293,46 @@ function draw_priority_set_table(self, container, priority_groups) { ); d3.select( - utils.get_ui_element_selector_by_role("priority-subclusters-export") + helpers.get_ui_element_selector_by_role("priority-subclusters-export") ).on("click", (d) => { helpers.export_json_button( - self.priority_groups_export(), + priority_groups_export(), clusterNetwork._defaultDateViewFormatSlider(self.today) ); }); d3.select( - utils.get_ui_element_selector_by_role("priority-subclusters-export-csv") + helpers.get_ui_element_selector_by_role("priority-subclusters-export-csv") ).on("click", (d) => { helpers.export_csv_button( - self.priority_groups_export_nodes(), + priority_groups_export_nodes(self), "clusters-of-interest" ); }); d3.select("#priority_set_table_download").on("click", (d) => { helpers.export_csv_button( - self.priority_groups_export_sets(), + priority_groups_export_sets(), "clusters_of_interest_table" ); }); } } +function redraw_tables(self) { + tables.update_volatile_elements(self, self.cluster_table); + if (self.subcluster_table) { + tables.update_volatile_elements(self, self.subcluster_table); + } + tables.update_volatile_elements(self, nodesTab.getNodeTable()); + if (self.priority_set_table) { + tables.update_volatile_elements(self, self.priority_set_table); + } +}; + 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 current_time = helpers.getCurrentDate(); let edge_length = options["priority-edge-length"] || self.subcluster_threshold; let reference_date = options["timestamp"] || self.today; @@ -1599,13 +2383,13 @@ function priority_set_view(self, priority_set, options) { let dco = "fee8c8fdbb84e34a33"; let defColorsOther = d3.scale .ordinal() - .range(_.map(_.range(0, dco.length, 6), (d) => "#" + dco.substr(d, 6))); + .range(_.map(_.range(0, dco.length, 6), (d) => "#" + dco.substring(d, 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))); + .range(_.map(_.range(0, dcpg.length, 6), (d) => "#" + dcpg.substring(d, d + 6))); let viewEnum = []; let dateID = {}; @@ -1674,7 +2458,7 @@ function priority_set_view(self, priority_set, options) { }, "computed-attributes": { date_added: { - depends: [timeDateUtil._networkCDCDateField], + depends: [helpers._networkCDCDateField], label: "Date added to cluster of interest", type: "Date", map: function (node) { @@ -1684,7 +2468,7 @@ function priority_set_view(self, priority_set, options) { }, }, priority_set: { - depends: [timeDateUtil._networkCDCDateField], + depends: [helpers._networkCDCDateField], label: "Cluster of Interest Status", enum: viewEnum, type: "String", @@ -1699,7 +2483,7 @@ function priority_set_view(self, priority_set, options) { if ( self._filter_by_date( reference_date, - timeDateUtil._networkCDCDateField, + helpers._networkCDCDateField, current_time, node, true @@ -1732,6 +2516,27 @@ function priority_set_view(self, priority_set, options) { }); } +function priority_groups_set_description( + self, + name, + description, + update_table +) { + if (defined_priority_groups) { + var idx = _.findIndex( + defined_priority_groups, + (g) => g.name === name + ); + if (idx >= 0) { + defined_priority_groups[idx].description = description; + priority_groups_update_node_sets(self, name, "update"); + if (update_table) { + draw_priority_set_table(self); + } + } + } +}; + function priority_groups_add_set( self, nodeset, @@ -1753,7 +2558,7 @@ function priority_groups_add_set( return true; } let my_nodes = new Set(_.map(nodeset.nodes, (d) => d.name)); - return _.some(self.defined_priority_groups, (d) => { + return _.some(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) { @@ -1779,28 +2584,28 @@ function priority_groups_add_set( op_code = op_code || "insert"; if (not_validated) { - self.priority_groups_validate([nodeset]); + priority_groups_validate(self, [nodeset]); } if (prior_name) { let prior_index = _.findIndex( - self.defined_priority_groups, + 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"); + priority_groups_update_node_sets(self, prior_name, "delete"); op_code = "insert"; } - self.defined_priority_groups[prior_index] = nodeset; + defined_priority_groups[prior_index] = nodeset; } else { if (check_dup()) return false; - self.defined_priority_groups.push(nodeset); + defined_priority_groups.push(nodeset); } } else { if (check_dup()) return false; - self.defined_priority_groups.push(nodeset); + defined_priority_groups.push(nodeset); } - self.priority_groups_update_node_sets(nodeset.name, op_code); + priority_groups_update_node_sets(self, nodeset.name, op_code); if (update_table) { draw_priority_set_table(self); @@ -1809,6 +2614,23 @@ function priority_groups_add_set( return true; } +function priority_groups_remove_set(self, name, update_table) { + if (defined_priority_groups) { + var idx = _.findIndex( + defined_priority_groups, + (g) => g.name === name + ); + if (idx >= 0) { + defined_priority_groups.splice(idx, 1); + priority_groups_update_node_sets(self, name, "delete"); + if (update_table) { + draw_priority_set_table(self); + } + } + } +}; + + function priority_set_inject_node_attibutes(self, nodes, node_attributes) { let attr_by_id = {}; _.each(node_attributes, (n, i) => { @@ -1825,15 +2647,247 @@ function priority_set_inject_node_attibutes(self, nodes, node_attributes) { }); } -function get_editor() { - return priority_set_editor; +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; } +function priority_groups_find_by_name(self, name) { + return defined_priority_groups?.find((pg) => pg.name === name); +}; + +function priority_group_get_all_events(self) { + // generate a set of all unique temporal events (when new data were added to ANY PG) + const events = new Set(); + if (defined_priority_groups) { + _.each(defined_priority_groups, (g) => { + _.each(g.nodes, (n) => { + events.add(clusterNetwork._defaultDateViewFormatSlider(n.added)); + }); + }); + } + return events; +}; + +function generate_coi_temporal_report(self, ref_set, D) { + if (!ref_set) return {}; + D = D || 0.005; + + const nodesD = hivtrace_cluster_depthwise_traversal( + self.json.Nodes, + self.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 = helpers.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([...priority_group_get_all_events(self)]); + network_events.reverse(); + const info_by_event = {}; + + _.each(network_events, (DT) => { + const event_date = clusterNetwork._defaultDateViewFormatSlider.parse(DT); + const event_date_m3y = clusterNetwork._defaultDateViewFormatSlider.parse(DT); + event_date_m3y.setFullYear(event_date.getFullYear() - 3); + const event_date_m1y = clusterNetwork._defaultDateViewFormatSlider.parse(DT); + event_date_m1y.setFullYear(event_date.getFullYear() - 1); + const n_filter = (n) => + self._filter_by_date( + beginning_of_time, + helpers._networkCDCDateField, + event_date, + n + ); + const n_filter3 = (n) => + self._filter_by_date( + event_date_m3y, + helpers._networkCDCDateField, + event_date, + n + ); + const n_filter1 = (n) => + self._filter_by_date( + event_date_m1y, + helpers._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, + clusterNetwork._defaultDateViewFormatSlider( + self.attribute_node_value_by_id(n, helpers._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; +}; + +function priority_groups_export_nodes( + self, + group_set, + include_unvalidated +) { + group_set = group_set || defined_priority_groups; + + return _.flatten( + _.map( + _.filter(group_set, (g) => include_unvalidated || g.validated), + (g) => { + //const refTime = g.modified.getTime(); + //console.log ("GROUP: ",g.name, " = ", g.modified); + + const exclude_nodes = new Set(g.not_in_network); + let cluster_detect_size = 0; + g.nodes.forEach((node) => { + if (node.added <= g.created) cluster_detect_size++; + }); + return _.map( + _.filter(g.nodes, (gn) => !exclude_nodes.has(gn.name)), + (gn) => ({ + eHARS_uid: gn.name, + cluster_uid: g.name, + cluster_ident_method: g.kind, + person_ident_method: gn.kind, + person_ident_dt: helpers.hivtrace_date_or_na_if_missing(gn.added), + new_linked_case: gn.autoadded + ? 1 + : 0, + cluster_created_dt: helpers.hivtrace_date_or_na_if_missing(g.created), + network_date: helpers.hivtrace_date_or_na_if_missing(self.today), + cluster_detect_size: cluster_detect_size, + cluster_type: g.createdBy, + cluster_modified_dt: helpers.hivtrace_date_or_na_if_missing(g.modified), + cluster_growth: clusterNetwork._cdcConciseTrackingOptions[g.tracking], + national_priority: g.meets_priority_def, + cluster_current_size: g.nodes.length, + cluster_dx_recent12_mo: g.last12, + cluster_overlap: g.overlap.sets, + }) + ); + } + ) + ); +}; + +function priority_groups_export_sets() { + return _.flatten( + _.map( + _.filter(defined_priority_groups, (g) => g.validated), + (g) => ({ + cluster_type: g.createdBy, + cluster_uid: g.name, + cluster_modified_dt: helpers.hivtrace_date_or_na_if_missing(g.modified), + cluster_created_dt: helpers.hivtrace_date_or_na_if_missing(g.created), + cluster_ident_method: g.kind, + cluster_growth: clusterNetwork._cdcConciseTrackingOptions[g.tracking], + cluster_current_size: g.nodes.length, + national_priority: g.meets_priority_def, + cluster_dx_recent12_mo: g.last12, + cluster_overlap: g.overlap.sets, + }) + ) + ); +}; + export { init, + get_editor, + get_pg, open_editor, + load_priority_sets, + priority_groups_compute_node_membership, + priority_groups_validate, priority_set_view, draw_priority_set_table, priority_set_inject_node_attibutes, - get_editor + priority_groups_find_by_name, }; \ No newline at end of file diff --git a/src/helpers.js b/src/helpers.js index 99e75c2..1ae5076 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -1,36 +1,27 @@ -var download = require("downloadjs"); - +// ============================== +// UI & HTML +// ============================== +const download = require("downloadjs"); const _OTHER = __("general")["other"]; const CATEGORY_UNIQUE_VALUE_LIMIT = 12; -function b64toBlob(b64, onsuccess, onerror) { - var img = new Image(); - - img.onerror = onerror; - - img.onload = function onload() { - var canvas = document.getElementById("hyphy-chart-canvas"); - canvas.width = img.width; - canvas.height = img.height; - - var ctx = canvas.getContext("2d"); - ctx.fillStyle = "#FFFFFF"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - - if (canvas.msToBlob) { - var blob = canvas.msToBlob(onsuccess); - onsuccess(blob); - window.navigator.msSaveBlob(blob, "image.png"); - } else { - canvas.toBlob(onsuccess); +function copyToClipboard(text) { + navigator.clipboard.writeText(text).then( + () => { + console.log("Copying to clipboard was successful!"); + }, + (err) => { + console.error("Could not copy text: ", err); } - }; - - img.src = b64; + ); } -var datamonkey_export_csv_button = function (data, name) { +function get_ui_element_selector_by_role(role) { + return ` [data-hivtrace-ui-role='${role}']`; +}; + +// TODO: consolidate export functions +function export_csv_button(data, name) { data = d3.csv.format(data); if (data !== null) { name = name ? name + ".csv" : "export.csv"; @@ -49,7 +40,7 @@ var datamonkey_export_csv_button = function (data, name) { } }; -var datamonkey_export_json_button = function (data, title) { +function export_json_button(data, title) { if (data !== null) { title = title || "export"; var pom = document.createElement("a"); @@ -67,7 +58,66 @@ var datamonkey_export_json_button = function (data, title) { } }; -var datamonkey_save_image = function (type, container) { +function export_handler(data, filename, mimeType) { + function msieversion() { + var ua = window.navigator.userAgent; + var msie = ua.indexOf("MSIE "); + // eslint-disable-next-line + if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./)) { + return true; + } + return false; + } + + if (msieversion()) { + var IEwindow = window.open(); + IEwindow.document.write(data); + IEwindow.document.close(); + IEwindow.document.execCommand("SaveAs", true, filename + ".csv"); + IEwindow.close(); + } else { + var pom = document.createElement("a"); + pom.setAttribute( + "href", + "data:" + + (mimeType || "text/plain") + + ";charset=utf-8," + + encodeURIComponent(data) + ); + pom.setAttribute("download", filename || "download.tsv"); + pom.click(); + pom.remove(); + } +} + +function b64toBlob(b64, onsuccess, onerror) { + var img = new Image(); + + img.onerror = onerror; + + img.onload = function onload() { + var canvas = document.getElementById("hyphy-chart-canvas"); + canvas.width = img.width; + canvas.height = img.height; + + var ctx = canvas.getContext("2d"); + ctx.fillStyle = "#FFFFFF"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + if (canvas.msToBlob) { + var blob = canvas.msToBlob(onsuccess); + onsuccess(blob); + window.navigator.msSaveBlob(blob, "image.png"); + } else { + canvas.toBlob(onsuccess); + } + }; + + img.src = b64; +} + +function save_image(type, container) { var prefix = { xmlns: "http://www.w3.org/2000/xmlns/", xlink: "http://www.w3.org/1999/xlink", @@ -89,7 +139,7 @@ var datamonkey_save_image = function (type, container) { } } } - } catch (e) { + } catch { console.log("Could not process stylesheet : " + ss); // eslint-disable-line } } @@ -173,95 +223,7 @@ var datamonkey_save_image = function (type, container) { } }; -function datamonkey_describe_vector(vector, as_list) { - let d; - - if (vector.length) { - vector.sort(d3.ascending); - - d = { - min: d3.min(vector), - max: d3.max(vector), - median: d3.median(vector), - Q1: d3.quantile(vector, 0.25), - Q3: d3.quantile(vector, 0.75), - mean: d3.mean(vector), - }; - } else { - d = { - min: null, - max: null, - median: null, - Q1: null, - Q3: null, - mean: null, - }; - } - - if (as_list) { - d = - "
    Range  :" +
    -      d["min"] +
    -      "-" +
    -      d["max"] +
    -      "\n" +
    -      "IQR    :" +
    -      d["Q1"] +
    -      "-" +
    -      d["Q3"] +
    -      "\n" +
    -      "Mean   :" +
    -      d["mean"] +
    -      "\n" +
    -      "Median :" +
    -      d["median"] +
    -      "\n" +
    -      "
    "; - - /*d = - "
    " + - "
    Range
    " + d['min'] + "-" + d['max'] + "
    " + - "
    IQR
    " + d['Q1'] + "-" + d['Q3'] + "
    " + - "
    Mean
    " + d['mean'] + "
    " + - "
    Median
    " + d['median'] + "
    ";*/ - } - - return d; -} - -function datamonkey_export_handler(data, filename, mimeType) { - function msieversion() { - var ua = window.navigator.userAgent; - var msie = ua.indexOf("MSIE "); - // eslint-disable-next-line - if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./)) { - return true; - } - return false; - } - - if (msieversion()) { - var IEwindow = window.open(); - IEwindow.document.write(data); - IEwindow.document.close(); - IEwindow.document.execCommand("SaveAs", true, filename + ".csv"); - IEwindow.close(); - } else { - var pom = document.createElement("a"); - pom.setAttribute( - "href", - "data:" + - (mimeType || "text/plain") + - ";charset=utf-8," + - encodeURIComponent(data) - ); - pom.setAttribute("download", filename || "download.tsv"); - pom.click(); - pom.remove(); - } -} - -function datamonkey_table_to_text(table_id, sep) { +function table_to_text(table_id, sep) { sep = sep || "\t"; var header_row = []; var extract_text = function (e) { @@ -322,9 +284,49 @@ function datamonkey_table_to_text(table_id, sep) { ); } -function get_unique_count(nodes, schema) { - let schema_keys = _.keys(schema); +function hivtrace_render_button_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 = table_to_text(table_tag, csv ? "," : "\t"); + file_name_placeholder = file_name_placeholder || table_tag.substring(1); + if (!csv) { + export_handler( + table_text, + file_name_placeholder + ".tsv", + "text/tab-separated-values" + ); + } else { + 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; +} + +// ============================== +// Graph & Data +// ============================== +function collapseLargeCategories(nodes, schema) { + let schema_keys = _.keys(schema); let new_obj = {}; _.each(schema_keys, (sk) => (new_obj[sk] = [])); @@ -337,49 +339,138 @@ function get_unique_count(nodes, schema) { }); }); - // Get uniques across all keys - return _.mapObject(new_obj, (val) => _.uniq(val).length); -} + let counts = _.mapObject(new_obj, (d) => _.countBy(d)); -function getUniqueValues(nodes, schema) { - let schema_keys = _.keys(schema); + // Sort and place everything after CATEGORY_UNIQUE_VALUE_LIMIT entries in 'Other' + // map object to counts + _.each(schema_keys, (sk) => { + let entries = Object.entries(counts[sk]); + let sorted = _.sortBy(entries, (d) => -d[1]); - let new_obj = {}; - _.each(schema_keys, (sk) => (new_obj[sk] = [])); + if (sorted.length > CATEGORY_UNIQUE_VALUE_LIMIT) { + let count = sorted[CATEGORY_UNIQUE_VALUE_LIMIT][1]; - // get attribute diversity to sort on later - let pa = _.map(nodes, (n) => _.omit(n.patient_attributes, "_id")); + // drop entries until we reach that value in sorted + let others = _.map(_.partition(sorted, (d) => d[1] <= count)[0], _.first); - _.each(pa, (p) => { - _.each(schema_keys, (sk) => { - new_obj[sk].push(p[sk]); - }); + // Remap all entries to "Other" + // Now take the entries in others and map to "Other" + _.each(nodes, (n) => { + if (_.contains(others, n["patient_attributes"][sk])) { + n["patient_attributes"][sk] = _OTHER; + } + }); + } }); - // Get uniques across all keys - return _.mapObject(new_obj, (val) => _.uniq(val)); + return true; } -function exportColorScheme(uniqValues, colorizer) { - let colors = _.map(uniqValues[colorizer.category_id], (d) => +/** + * @param {Object} uniqValues object consisting of all node categories (attributes), with key as the category_id and value as the set of unique values + * @param {*} colorizer maps current category to the displayed colors + * @returns {Object} value-color mapping of the currently selected "color by" node category (as selected by the graph UI bar) + */ +function getCurrentCategoryColorMapping(uniqValues, colorizer) { + const currentCategoryUniqueValues = uniqValues[colorizer.category_id]; + const currentCategoryCorrespondingColors = _.map(currentCategoryUniqueValues, (d) => colorizer.category(d) ); - return _.object(uniqValues[colorizer.category_id], colors); + return _.object(currentCategoryUniqueValues, currentCategoryCorrespondingColors); } -function copyToClipboard(text) { - navigator.clipboard.writeText(text).then( - () => { - console.log("Copying to clipboard was successful!"); - }, - () => { - console.error("Could not copy text: ", err); - } - ); +function describe_vector(vector, as_list) { + let d; + + if (vector.length) { + vector.sort(d3.ascending); + + d = { + min: d3.min(vector), + max: d3.max(vector), + median: d3.median(vector), + Q1: d3.quantile(vector, 0.25), + Q3: d3.quantile(vector, 0.75), + mean: d3.mean(vector), + }; + } else { + d = { + min: null, + max: null, + median: null, + Q1: null, + Q3: null, + mean: null, + }; + } + + if (as_list) { + d = + "
    Range  :" +
    +      d["min"] +
    +      "-" +
    +      d["max"] +
    +      "\n" +
    +      "IQR    :" +
    +      d["Q1"] +
    +      "-" +
    +      d["Q3"] +
    +      "\n" +
    +      "Mean   :" +
    +      d["mean"] +
    +      "\n" +
    +      "Median :" +
    +      d["median"] +
    +      "\n" +
    +      "
    "; + + /*d = + "
    " + + "
    Range
    " + d['min'] + "-" + d['max'] + "
    " + + "
    IQR
    " + d['Q1'] + "-" + d['Q3'] + "
    " + + "
    Mean
    " + d['mean'] + "
    " + + "
    Median
    " + d['median'] + "
    ";*/ + } + + return d; } -function collapseLargeCategories(nodes, schema) { +// hacky enums +const HIVTRACE_UNDEFINED = {}; +const HIVTRACE_TOO_LARGE = {}; +const HIVTRACE_PROCESSING = {}; +function format_value(value, formatter) { + if (typeof value === "undefined") { + return "Not computed"; + } + if (value === HIVTRACE_UNDEFINED) { + return "Undefined"; + } + if (value === HIVTRACE_TOO_LARGE) { + return "Size limit"; + } + + if (value === HIVTRACE_PROCESSING) { + return ''; + } + + return formatter ? formatter(value) : value; +} + +/** + * Categorize an edge by its edge length (pairwise distance) and whether or not it is below a threshold. + * @param {*} edge + * @param {Array} edge_types an array representing two edge types + * @param {Number} threshold + * @returns an element of edge_types + */ +function get_edge_type(edge, edge_types, threshold) { + return edge.length <= threshold ? edge_types[0] : edge_types[1]; +}; + +function get_unique_values(nodes, schema) { let schema_keys = _.keys(schema); + let new_obj = {}; _.each(schema_keys, (sk) => (new_obj[sk] = [])); @@ -392,41 +483,173 @@ function collapseLargeCategories(nodes, schema) { }); }); - let counts = _.mapObject(new_obj, (d) => _.countBy(d)); + // Get uniques across all keys + return _.mapObject(new_obj, (val) => _.uniq(val)); +} - // Sort and place everything after 15 entries in 'Other' - // map object to counts - _.each(schema_keys, (sk) => { - let entries = Object.entries(counts[sk]); - let sorted = _.sortBy(entries, (d) => -d[1]); +// TODO: review if is this right? +function get_unique_count(nodes, schema) { + return _.mapObject(get_unique_values(nodes, schema), (val) => val.length); +} - if (sorted.length > CATEGORY_UNIQUE_VALUE_LIMIT) { - let count = sorted[CATEGORY_UNIQUE_VALUE_LIMIT][1]; +// TODO: convert and save this data rather than do it each time. +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] = []; + }); - // drop entries until we reach that value in sorted - let others = _.map(_.partition(sorted, (d) => d[1] <= count)[0], _.first); + if (edge_filter) { + edges = _.filter(edges, edge_filter); + } - // Remap all entries to "Other" - // Now take the entries in others and map to "Other" - _.each(nodes, (n) => { - if (_.contains(others, n["patient_attributes"][sk])) { - n["patient_attributes"][sk] = _OTHER; + 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 true; + return clusters; +}; + +// ============================== +// Date & Time +// ============================== +const _networkCDCDateField = "hiv_aids_dx_dt"; +const _networkTimeQuery = /([0-9]{8}):([0-9]{8})/i; +const _defaultDateViewFormatExport = d3.time.format("%m/%d/%Y"); + +let cluster_time_scale; + +function dateTimeInit(options, isCDC) { + cluster_time_scale = options?.["cluster-time"]; + + if (isCDC && !cluster_time_scale) { + cluster_time_scale = _networkCDCDateField; + } } -module.exports.export_csv_button = datamonkey_export_csv_button; -module.exports.export_json_button = datamonkey_export_json_button; -module.exports.save_image = datamonkey_save_image; -module.exports.describe_vector = datamonkey_describe_vector; -module.exports.table_to_text = datamonkey_table_to_text; -module.exports.export_handler = datamonkey_export_handler; -module.exports.get_unique_count = get_unique_count; -module.exports.getUniqueValues = getUniqueValues; -module.exports.exportColorScheme = exportColorScheme; -module.exports.copyToClipboard = copyToClipboard; -module.exports.collapseLargeCategories = collapseLargeCategories; +function getClusterTimeScale() { + return cluster_time_scale; +} + +function getCurrentDate() { + return new Date(); +}; + +function getAncientDate() { + return new Date(1900, 0, 1); +}; + +function getNMonthsAgo(reference_date, months) { + var past_date = new Date(reference_date); + var past_months = past_date.getMonth(); + var diff_year = Math.floor(months / 12); + var left_over = months - diff_year * 12; + + if (left_over > past_months) { + past_date.setFullYear(past_date.getFullYear() - diff_year - 1); + past_date.setMonth(12 - (left_over - past_months)); + } else { + past_date.setFullYear(past_date.getFullYear() - diff_year); + past_date.setMonth(past_months - left_over); + } + + //past_date.setTime (past_date.getTime () - months * 30 * 24 * 3600000); + return past_date; +} + +function hivtrace_date_or_na_if_missing(date, formatter) { + formatter = formatter || _defaultDateViewFormatExport; + if (date) { + return formatter(date); + } + return "N/A"; +}; + +module.exports = { + // UI & HTML + copyToClipboard, + get_ui_element_selector_by_role, + export_csv_button, + export_json_button, + export_handler, + save_image, + table_to_text, + render_button_export_table_to_text: hivtrace_render_button_export_table_to_text, + + // Graph & Data + collapseLargeCategories, + getCurrentCategoryColorMapping, + describe_vector, + format_value, + get_edge_type, + get_unique_values, + get_unique_count, + hivtrace_cluster_depthwise_traversal, + HIVTRACE_UNDEFINED, + HIVTRACE_TOO_LARGE, + HIVTRACE_PROCESSING, + + // Date & Time + _networkCDCDateField, + _networkTimeQuery, + dateTimeInit, + getClusterTimeScale, + getCurrentDate, + getAncientDate, + getNMonthsAgo, + hivtrace_date_or_na_if_missing, +} \ No newline at end of file diff --git a/src/hivtrace.js b/src/hivtrace.js index 8fdb687..76e616c 100644 --- a/src/hivtrace.js +++ b/src/hivtrace.js @@ -4,7 +4,7 @@ import { hivtraceClusterGraphSummary } from "./hivtraceClusterGraphSummary.js"; import { histogram, histogramDistances } from "./histogram.js"; import { scatterPlot } from "./scatterplot.js"; -const misc = require("./misc.js"); +const svgPlots = require("./svgPlots.js"); const helpers = require("./helpers.js"); const colorPicker = require("./colorPicker.js"); const graphSummary = hivtraceClusterGraphSummary; @@ -15,7 +15,7 @@ export { histogram, histogramDistances, helpers, - misc, + svgPlots, colorPicker, scatterPlot, }; diff --git a/src/hivtraceClusterGraphSummary.js b/src/hivtraceClusterGraphSummary.js index 1b8cdb6..6db2f08 100644 --- a/src/hivtraceClusterGraphSummary.js +++ b/src/hivtraceClusterGraphSummary.js @@ -1,9 +1,5 @@ -var _ = require("underscore"); -var d3 = require("d3"); -var helpers = require("./helpers"); - -var _defaultFloatFormat = d3.format(",.2r"); -var _defaultPercentFormat = d3.format(",.3p"); +const helpers = require("./helpers"); +const clusterNetwork = require('./clusternetwork'); // The function for creating the "Network Statistics" table that is displayed on the "Statistics" tab. var hivtraceClusterGraphSummary = function (graph, tag, not_CDC) { @@ -41,11 +37,11 @@ var hivtraceClusterGraphSummary = function (graph, tag, not_CDC) { table_data.push([__("statistics")["links_per_node"], ""]); table_data.push([ "  " + __("statistics")["mean"] + "", - _defaultFloatFormat(degrees["mean"]), + clusterNetwork._defaultFloatFormat(degrees["mean"]), ]); table_data.push([ "  " + __("statistics")["median"] + "", - _defaultFloatFormat(degrees["median"]), + clusterNetwork._defaultFloatFormat(degrees["median"]), ]); table_data.push([ "  " + __("statistics")["range"] + "", @@ -60,11 +56,11 @@ var hivtraceClusterGraphSummary = function (graph, tag, not_CDC) { table_data.push([__("statistics")["cluster_sizes"], ""]); table_data.push([ "  " + __("statistics")["mean"] + "", - _defaultFloatFormat(degrees["mean"]), + clusterNetwork._defaultFloatFormat(degrees["mean"]), ]); table_data.push([ "  " + __("statistics")["median"] + "", - _defaultFloatFormat(degrees["median"]), + clusterNetwork._defaultFloatFormat(degrees["median"]), ]); table_data.push([ "  " + __("statistics")["range"] + "", @@ -82,23 +78,23 @@ var hivtraceClusterGraphSummary = function (graph, tag, not_CDC) { table_data.push(["Genetic distances (links only)", ""]); table_data.push([ "  " + __("statistics")["mean"] + "", - _defaultPercentFormat(degrees["mean"]), + clusterNetwork._defaultPercentFormat(degrees["mean"]), ]); table_data.push([ "  " + __("statistics")["median"] + "", - _defaultPercentFormat(degrees["median"]), + clusterNetwork._defaultPercentFormat(degrees["median"]), ]); table_data.push([ "  " + __("statistics")["range"] + "", - _defaultPercentFormat(degrees["min"]) + + clusterNetwork._defaultPercentFormat(degrees["min"]) + " - " + - _defaultPercentFormat(degrees["max"]), + clusterNetwork._defaultPercentFormat(degrees["max"]), ]); table_data.push([ "  " + __("statistics")["interquartile_range"] + "range", - _defaultPercentFormat(degrees["Q1"]) + + clusterNetwork._defaultPercentFormat(degrees["Q1"]) + " - " + - _defaultPercentFormat(degrees["Q3"]), + clusterNetwork._defaultPercentFormat(degrees["Q3"]), ]); } diff --git a/src/scatterplot.js b/src/scatterplot.js index e1976dc..225c469 100644 --- a/src/scatterplot.js +++ b/src/scatterplot.js @@ -1,8 +1,8 @@ var d3 = require("d3"); function hivtrace_render_scatterplot(points, w, h, id, labels, dates) { - var _defaultFloatFormat = d3.format(",.2r"); - var _defaultDateViewFormatShort = d3.time.format("%B %Y"); + const _defaultFloatFormat = d3.format(",.2r"); + const _defaultDateViewFormatShort = d3.time.format("%B %Y"); var margin = { top: 10, diff --git a/src/misc.js b/src/svgPlots.js similarity index 82% rename from src/misc.js rename to src/svgPlots.js index eefc226..843e471 100644 --- a/src/misc.js +++ b/src/svgPlots.js @@ -1,8 +1,4 @@ -var d3 = require("d3"), - _ = require("underscore"), - helpers = require("./helpers.js"); - -var hivtrace_generate_svg_polygon_lookup = {}; +const hivtrace_generate_svg_polygon_lookup = {}; _.each(_.range(3, 20), (d) => { var angle_step = (Math.PI * 2) / d; @@ -37,7 +33,7 @@ function hivtrace_generate_svg_symbol(type) { } } -var hivtrace_generate_svg_ellipse = function () { +function hivtrace_generate_svg_ellipse() { var self = this; self.ellipse = function () { @@ -73,7 +69,7 @@ var hivtrace_generate_svg_ellipse = function () { return self.ellipse; }; -var hivtrace_generate_svg_polygon = function () { +function hivtrace_generate_svg_polygon() { var self = this; self.polygon = function () { @@ -86,7 +82,7 @@ var hivtrace_generate_svg_polygon = function () { } else { var angle_step = (Math.PI * 2) / self.sides, current_angle = 0; - for (i = 0; i < self.sides - 1; i++) { + for (let i = 0; i < self.sides - 1; i++) { current_angle += angle_step; path += " L" + @@ -129,76 +125,6 @@ var hivtrace_generate_svg_polygon = function () { return self.polygon; }; -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++; - } -} - -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; -} - -function hivtrace_format_value(value, formatter) { - if (typeof value === "undefined") { - return "Not computed"; - } - if (value === hivtrace_undefined) { - return "Undefined"; - } - if (value === hivtrace_too_large) { - return "Size limit"; - } - - if (value === hivtrace_processing) { - return ''; - } - - return formatter ? formatter(value) : value; -} - function hivtrace_plot_cluster_dynamics( time_series, container, @@ -685,79 +611,7 @@ function hivtrace_plot_cluster_dynamics( .text(y_title); // beta - alpha } -// TODO: convert and save this data rather than do it each time. -var hivtrace_cluster_depthwise_traversal = function ( - 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 (err) { - 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; -}; - -function hivtrace_coi_timeseries(cluster, element, plot_width) { +function hivtrace_plot_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( @@ -956,20 +810,8 @@ function hivtrace_coi_timeseries(cluster, element, plot_width) { }); } -function edge_typer(e, edge_types, T) { - return edge_types[e.length <= T ? 0 : 1]; -}; - 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, - undefined: {}, - too_large: {}, - processing: {}, - format_value: hivtrace_format_value, - symbol: hivtrace_generate_svg_symbol, - cluster_dynamics: hivtrace_plot_cluster_dynamics, - hivtrace_cluster_depthwise_traversal, + generate_svg_symbol: hivtrace_generate_svg_symbol, + plot_coi_timeseries: hivtrace_plot_coi_timeseries, + plot_cluster_dynamics: hivtrace_plot_cluster_dynamics, } diff --git a/src/tables.js b/src/tables.js index e937bd2..ba24247 100644 --- a/src/tables.js +++ b/src/tables.js @@ -1,13 +1,15 @@ -const d3 = require("d3"); -const _ = require("underscore"); -const utils = require("./utils.js"); -const timeDateUtil = require('./timeDateUtil.js'); -const nodesTab = require('./nodesTab.js'); +import jsConvert from "js-convert-case"; +import * as clusterNetwork from "./clusternetwork.js"; +import * as helpers from "./helpers.js"; +import * as nodesTab from "./nodesTab.js"; +import * as clustersOfInterest from "./clustersOI/clusterOI.js"; const _networkNodeIDField = "hivtrace_node_id"; const _networkNewNodeMarker = "[+]"; +const _networkDotFormatPadder = d3.format("08d"); function add_a_sortable_table( + self, container, headers, content, @@ -49,7 +51,7 @@ function add_a_sortable_table( .append("td") .call((selection) => selection.each(function (d, i) { set_table_elements(d, this); - format_a_cell(d, i, this, priority_set_editor); + format_a_cell(self, d, i, this, priority_set_editor); })); container.node().appendChild(tbody.node()); } @@ -69,7 +71,7 @@ function add_a_sortable_table( .append("th") .call((selection) => selection.each(function (d, i) { set_table_elements(d, this); - format_a_cell(d, i, this, priority_set_editor); + format_a_cell(self, d, i, this, priority_set_editor); })); } //'Showing --/-- network nodes'); @@ -79,10 +81,10 @@ function add_a_sortable_table( table_caption.enter().insert("caption", ":first-child"); table_caption.html((d) => d); table_caption - .select(utils.get_ui_element_selector_by_role("table-count-total")) + .select(helpers.get_ui_element_selector_by_role("table-count-total")) .text(content.length); table_caption - .select(utils.get_ui_element_selector_by_role("table-count-shown")) + .select(helpers.get_ui_element_selector_by_role("table-count-shown")) .text(content.length); } } @@ -91,7 +93,7 @@ function table_get_cell_value(data) { return _.isFunction(data.value) ? data.value() : data.value; } -function format_a_cell(data, index, item, priority_set_editor) { +function format_a_cell(self, data, index, item, priority_set_editor) { var this_sel = d3.select(item); var current_value = table_get_cell_value(data); var handle_sort = this_sel; @@ -99,7 +101,7 @@ function format_a_cell(data, index, item, priority_set_editor) { handle_sort.selectAll("*").remove(); if ("callback" in data) { - handle_sort = data.callback(item, current_value); + handle_sort = data.callback(self, item, current_value); } else { var repr = "format" in data ? data.format(current_value) : current_value; if ("html" in data && data.html) this_sel.html(repr); @@ -154,23 +156,19 @@ function format_a_cell(data, index, item, priority_set_editor) { var search_form_generator = function () { return ( - `
    + `
    - +
    - -
    +
    +
    Type in text to select columns which contain the term.
    @@ -213,17 +211,25 @@ function format_a_cell(data, index, item, priority_set_editor) { "#" + d3.select(this).attr("aria-describedby") ); var search_click = popover_div.selectAll( - utils.get_ui_element_selector_by_role("table-filter-apply") + helpers.get_ui_element_selector_by_role("table-filter-apply") ); var reset_click = popover_div.selectAll( - utils.get_ui_element_selector_by_role("table-filter-reset") - ); - var search_box = popover_div.selectAll( - utils.get_ui_element_selector_by_role("table-filter-term") + helpers.get_ui_element_selector_by_role("table-filter-reset") ); + var search_box_element = helpers.get_ui_element_selector_by_role("table-filter-term"); + var search_box = popover_div.selectAll(search_box_element); search_box.property("value", data.filter_term); + // search by hitting enter + $(search_box_element).on("keyup", (e) => { + if (e.key === "Enter") { + update_term(search_box.property("value")); + filter_table(clicker.node()); + } + }) + + // search by clicking the search icon search_click.on("click", (d) => { update_term(search_box.property("value")); filter_table(clicker.node()); @@ -238,7 +244,7 @@ function format_a_cell(data, index, item, priority_set_editor) { } if (handle_sort && "sort" in data) { - var clicker = handle_sort + clicker = handle_sort .append("a") .property("href", "#") .on("click", function (d) { @@ -449,7 +455,7 @@ function filter_table(element) { }); d3.select(table_element[0]) .select("caption") - .select(utils.get_ui_element_selector_by_role("table-count-shown")) + .select(helpers.get_ui_element_selector_by_role("table-count-shown")) .text(shown_rows); /*.selectAll("td").each (function (d, i) { @@ -505,11 +511,11 @@ function filter_parse(filter_value) { if (d[0] === '"' && d[d.length - 1] === '"' && d.length > 2) { return { type: "re", - value: new RegExp("^" + d.substr(1, d.length - 2) + "$", "i"), + value: new RegExp("^" + d.substring(1, d.length - 1) + "$", "i"), }; } if (d[0] === "<" || d[0] === ">") { - var distance_threshold = parseFloat(d.substr(1)); + var distance_threshold = parseFloat(d.substring(1)); if (distance_threshold > 0) { return { type: "distance", @@ -518,8 +524,8 @@ function filter_parse(filter_value) { }; } } - if (timeDateUtil.getClusterTimeScale()) { - var is_range = timeDateUtil._networkTimeQuery.exec(d); + if (helpers.getClusterTimeScale()) { + var is_range = helpers._networkTimeQuery.exec(d); if (is_range) { return { type: "date", @@ -611,12 +617,851 @@ function sort_table_toggle_icon(element, value) { } } -module.exports = { +function update_volatile_elements(self, 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? + format_a_cell(self, d, i, this); + }); +}; + +function _node_table_draw_buttons(self, 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]) { + self.collapse_cluster(self.clusters[payload[3] - 1], true); + } else { + self.expand_cluster(self.clusters[payload[3] - 1]); + } + //format_a_cell(self, d3.select(element).datum(), null, element); + update_volatile_elements(self, nodesTab.getNodeTable()); + } + }); + buttons.each(function (d, e) { + if (d.length >= 3) { + d3.select(this).classed("btn-primary", false).classed(d[2], true); + } + }); +} + +function _cluster_table_draw_id(self, 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(self, 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; + + /*if (moved) { + if (d > 0) { + return "" + moved + " nodes moved +" + d + " new"; + } else { + if (d === 0) { + return "" + moved + " nodes moved"; + } else { + return "" + moved + " nodes moved " + (-d) + " removed"; + } + } + + } else { + if (d > 0) { + return "+" + d + " nodes"; + } else { + if (d === 0) { + return "no size change"; + } else { + return "" + (-d) + " nodes removed"; + } + } + }*/ + + 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]) { + self.expand_cluster(self.clusters[payload[3] - 1], true); + } else { + self.collapse_cluster(self.clusters[payload[3] - 1]); + } + update_volatile_elements(self, self.cluster_table); + if (self.subcluster_table) { + update_volatile_elements(self, 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) && self._is_new_node(e.source)) + return "Newly added"; + if (_.isObject(e.target) && self._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 draw_extended_node_table( + self, + 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 (self, 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 self.json.patient_attribute_schema + ? self.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; + draw_extended_node_table( + self, + 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 === clusterNetwork._networkMissing) { + return v; + } + return clusterNetwork._defaultDateViewFormatSlider(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; + }); + + draw_node_table( + self, + null, + null, + [table_headers], + table_rows, + container, + 'Showing --/-- network nodes' + ); + } +}; + +function draw_node_table( + self, + 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 { + 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; + }); + } + + add_a_sortable_table( + self, + container, + headers, + rows, + true, + table_caption, + clustersOfInterest.get_editor() + // rows + ); + } +}; + +function draw_cluster_table(self, 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(clusterNetwork._networkSubclusterSeparator), + (ss) => _networkDotFormatPadder(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"] + + "
    " + + __("statistics")["mean"] + + "[" + + __("statistics")["median"] + + ", IQR]", + html: true, + }); + + headers[0].push({ + value: + __("statistics")["genetic_distances_among_linked_nodes"] + + "
    " + + __("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) => { + function make_row(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"], + helpers.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"], + helpers.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 ( + clusterNetwork._defaultFloatFormat(d["mean"]) + + " [" + + clusterNetwork._defaultFloatFormat(d["median"]) + + ", " + + clusterNetwork._defaultFloatFormat(d["Q1"]) + + " - " + + clusterNetwork._defaultFloatFormat(d["Q3"]) + + "]" + ); + } catch { + return ""; + } + }, + }); + this_row.push({ + value: d.distances, + format: function (d) { + try { + return ( + clusterNetwork._defaultFloatFormat(d["mean"]) + + " [" + + clusterNetwork._defaultFloatFormat(d["median"]) + + ", " + + clusterNetwork._defaultFloatFormat(d["Q1"]) + + " - " + + clusterNetwork._defaultFloatFormat(d["Q3"]) + + "]" + ); + } catch { + 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)); + }); + } + }); + + add_a_sortable_table( + self, + element, + headers, + rows, + true, + options && options["caption"] ? options["caption"] : null, + clustersOfInterest.get_editor() + ); + } +}; + +export { _networkNodeIDField, _networkNewNodeMarker, add_a_sortable_table, - format_a_cell, - sort_table_by_column, - sort_table_toggle_icon, -} - + filter_parse, + update_volatile_elements, + draw_extended_node_table, + draw_node_table, + draw_cluster_table, +}; \ No newline at end of file diff --git a/src/timeDateUtil.js b/src/timeDateUtil.js deleted file mode 100644 index e98c518..0000000 --- a/src/timeDateUtil.js +++ /dev/null @@ -1,34 +0,0 @@ -const _networkCDCDateField = "hiv_aids_dx_dt"; -const _networkTimeQuery = /([0-9]{8}):([0-9]{8})/i; - -let cluster_time_scale; - -function init(options, isCDC) { - cluster_time_scale = options?.["cluster-time"]; - - if (isCDC && !cluster_time_scale) { - cluster_time_scale = _networkCDCDateField; - } -} - -function getClusterTimeScale() { - return cluster_time_scale; -} - -function getCurrentDate() { - return new Date(); -}; - -function getAncientDate() { - return new Date(1900, 0, 1); -}; - - -module.exports = { - _networkCDCDateField, - _networkTimeQuery, - getClusterTimeScale, - getCurrentDate, - getAncientDate, - init -} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 8e6b127..0000000 --- a/src/utils.js +++ /dev/null @@ -1,7 +0,0 @@ -function get_ui_element_selector_by_role(role) { - return ` [data-hivtrace-ui-role='${role}']`; -}; - -module.exports = { - get_ui_element_selector_by_role, -}; \ No newline at end of file diff --git a/ui-tests/clusterOI.spec.js b/ui-tests/clusterOI.spec.js index b00e33f..7f7d9ab 100644 --- a/ui-tests/clusterOI.spec.js +++ b/ui-tests/clusterOI.spec.js @@ -6,18 +6,35 @@ test.beforeEach(async ({ page }) => { errors = []; page.on('console', msg => { if (msg.type() === 'error') { - console.log(msg.text()); errors.push(msg.text()); } }) + page.on("pageerror", (err) => { + errors.push(err.message); + }) await page.goto('http://127.0.0.1:8080/html/priority-sets-args.html?network=../ui-tests/data/network.json'); }); -test.afterEach(({ page }) => { +test.afterEach(async ({ page }) => { expect(errors).toEqual([]); }); +/** + * Returns a function that accepts a dialog and conditionally dismisses it + * @param {*} message message that the dialog should contain to be dismissed + * @returns function that accepts a dialog and dismisses it if the message matches + */ +const getAcceptDialogFunction = (message) => { + return async (dialog) => { + if (dialog.message().includes(message)) { + await dialog.accept(); + } else { + await dialog.dismiss(); + } + } +} + const openEditor = async (page) => { // go to clusterOI tab await expect(page.locator("#priority-set-tab")).toBeVisible(); @@ -35,8 +52,10 @@ const openEditor = async (page) => { await expect(jsPanels).toHaveLength(1); } -const createCluster = async (page, nodes) => { - await openEditor(page); +const createCluster = async (page, nodes, editorOpen = false) => { + if (!editorOpen) { + await openEditor(page); + } for (const node of nodes) { await page.locator('[data-hivtrace-ui-role="priority-panel-nodeids"]').fill(node); @@ -64,12 +83,23 @@ const previewClusterOI = async (page) => { * Assumes that the clusterOI editor has nodes * Assumes that a current clusterOI does not have the same name */ -const saveClusterOI = async (page, name) => { +const saveClusterOI = async (page, name, expectDialog = false, newCluster = true) => { await page.locator("#priority-panel-save").click(); - await expect(page.locator(".has-error")).toBeVisible(); + if (newCluster) { + // user gets prompted to enter cluster name + await expect(page.locator(".has-error")).toBeVisible(); + await page.locator('[data-hivtrace-ui-role="priority-panel-name"]').fill(name); + } + + if (expectDialog) { + const acceptDialog = getAcceptDialogFunction("This cluster of interest does not include all the nodes in the current"); + await page.on('dialog', acceptDialog); + await page.locator("#priority-panel-save").click(); + await page.off('dialog', acceptDialog); + } else { + await page.locator("#priority-panel-save").click(); + } - await page.locator('[data-hivtrace-ui-role="priority-panel-name"]').fill(name); - await page.locator("#priority-panel-save").click(); await expect(page.locator(".has-error")).toHaveCount(0); // jspanel should not be visible @@ -83,27 +113,31 @@ const saveClusterOI = async (page, name) => { .toBeVisible(); }; +const deleteClusterOI = async (page, name) => { + await page.locator("#priority_set_table") + .filter({ has: page.getByText(name) }) + .first() + .locator(".view-edit-cluster") + .first() + .click(); + + const acceptDialog = getAcceptDialogFunction("This action cannot be undone. Proceed?") + await page.on('dialog', acceptDialog); + await page.getByText("Delete this cluster of interest", { exact: true }).first().click(); + await page.off('dialog', acceptDialog); + + await expect(await page.locator("#priority_set_table") + .filter({ has: page.getByText(name) })) + .toHaveCount(0); +} test('clusterOI editor opens, can add nodes', async ({ page }) => { // these specific nodes cause a confirm to appear when trying to save the clusterOI await createCluster(page, ["BMK384750US2015", "BMK385560US2007"]); - - const acceptDialog = async (dialog) => { - if (dialog.message().includes("This cluster of interest does not include all the nodes in the current")) { - await dialog.accept(); - } else { - await dialog.dismiss(); - } - } - - await page.on('dialog', acceptDialog); - await saveClusterOI(page, "Cluster 1"); - await page.off('dialog', acceptDialog); + await saveClusterOI(page, "Cluster 1", true); }); test('preview cluster and then open clusterOI editor', async ({ page }) => { - await page.goto('http://127.0.0.1:8080/html/priority-sets-args.html?network=../ui-tests/data/network.json'); - await page.locator(".cluster-group").first().click(); await page.getByText("Show this cluster in separate tab", { exact: true }).click(); await expect(page.locator("#top_level_tab_container") @@ -128,4 +162,145 @@ test('add nodes via graph to clusterOI editor and save', async ({ page }) => { await previewClusterOI(page); await saveClusterOI(page, "Cluster 1"); -}); \ No newline at end of file +}); + +test('add nodes via graph to clusterOI editor, save, clone clusterOI, save, and delete all', async ({ page }) => { + // add nodes via graph to clusterOI editor and save + await openEditor(page); + + await page.locator("#trace-default-tab").click(); + await page.locator(".cluster-group").first().click(); + await page.getByText("Add this cluster to the cluster of interest", { exact: true }).click(); + + await previewClusterOI(page); + + await saveClusterOI(page, "Cluster 1"); + + // clone clusterOI and save + await page.locator(".view-edit-cluster").first().click(); + await page.getByText("Clone this cluster of interest in a new editor panel", { exact: true }).click(); + + const jsPanels = await page.locator(".jsPanel").all(); + await expect(jsPanels).toHaveLength(1); + + await createCluster(page, ["01_AEMK272426TH2015"], true); + + await saveClusterOI(page, "Cluster 2", true); + + await expect(await page.locator("#priority_set_table") + .filter({ has: page.getByText("Cluster 1") }) + .filter({ has: page.getByText("Cluster 2") })) + .toBeVisible(); + + // delete clusters, accept confirmation dialog + await deleteClusterOI(page, "Cluster 1"); + + await expect(await page.locator("#priority_set_table") + .filter({ has: page.getByText("Cluster 2") })) + .toHaveCount(1); + + // delete the last cluster + await deleteClusterOI(page, "Cluster 2"); + + await expect(await page.locator("#priority_set_table tbody tr")) + .toHaveCount(0); +}) + +test('create clusterOI and save, then view history', async ({ page }) => { + await openEditor(page); + + await page.locator("#trace-default-tab").click(); + await page.locator(".cluster-group").first().click(); + await page.getByText("Add this cluster to the cluster of interest", { exact: true }).click(); + + // check that the nodes are added, empty clusterOI text shouldn't exist + await expect(page.getByText("clusterOI editor (0 nodes)")).toHaveCount(0); + + await previewClusterOI(page); + + await saveClusterOI(page, "Cluster 1"); + + // view history + await page.locator(".view-edit-cluster").first().click(); + await page.getByText("View history over time", { exact: true }).click(); + + await expect(page.getByText("History of Cluster 1")).toBeVisible(); + + await expect(page.locator("div.tab-pane.active") + .filter({ has: page.locator("svg") }) // history over time graph + .filter({ has: page.getByText("BMK383478US2007") }) + .filter({ has: page.getByText("BMK383725US2014") })) + .toBeVisible(); +}) + +test('create clusterOI, view nodes list, edit, and view nodes list again', async ({ page }) => { + await openEditor(page); + + await page.locator("#trace-default-tab").click(); + await page.locator(".cluster-group").first().click(); + await page.getByText("Add this cluster to the cluster of interest", { exact: true }).click(); + + // check that the nodes are added, empty clusterOI text shouldn't exist + await expect(page.getByText("clusterOI editor (0 nodes)")).toHaveCount(0); + + await previewClusterOI(page); + + await saveClusterOI(page, "Cluster 1"); + + // view nodes list + await page.locator(".view-edit-cluster").first().click(); + await page.getByText("View nodes in this cluster of interest", { exact: true }).click(); + + await page.locator("[data-hivtrace-ui-role='cluster_list_view_toggle']").click(); + await expect(page.locator("[data-hivtrace-ui-role='cluster_list_body']") + .filter({ has: page.getByText("BMK384750US2015") })) + .toBeVisible(); + + await expect(page.getByText("BMK384750US2015")).toHaveCount(3); // Two in the node view, one hidden in the nodes table + + // edit nodes + await page.locator("[data-hivtrace-ui-role='cluster_list_body']").getByText("Dismiss", { exact: true }).click(); + await page.locator(".view-edit-cluster").first().click(); + await page.getByText("Modify this cluster of interest", { exact: true }).click(); + + await page.locator("#priority-panel-node-table") + .locator("tr") + .filter({ has: page.getByText("BMK384750US2015") }) + .locator("i.fa.fa-trash") + .click(); + + // TODO: determine why the page is sometimes getting redirected (never happens during manual testing) + // wait two seconds because of unpredictable delete button page redirection behavior + await page.waitForTimeout(2000); + + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.locator(".jsPanel") + .first() + .filter({ has: page.getByText("BMK384750US2015") })) + .toHaveCount(0); + + await expect(page.locator(".jsPanel") + .first() + .filter({ has: page.getByText("BMK384168US2005") })) + .toHaveCount(1); + + await saveClusterOI(page, "Cluster 1", false, false); + + // expect node size to be 42 + await expect(page.locator("#priority_set_table") + .filter({ has: page.getByText("Cluster 1") }) + .filter({ has: page.getByText("42") })) + .toBeVisible(); + + // view nodes list again and check that the node is not there + await page.locator(".view-edit-cluster").first().click(); + await page.getByText("View nodes in this cluster of interest", { exact: true }).click(); + + await expect(page.locator("[data-hivtrace-ui-role='cluster_list_body']") + .filter({ has: page.getByText("BMK384750US2015") })) + .toHaveCount(0); + + await expect(page.getByText("BMK384750US2015")).toHaveCount(1); // Only the one hidden in the nodes table + await expect(page.getByText("BMK384168US2005")).toHaveCount(3); // Two in the node view, one hidden in the nodes table +}) \ No newline at end of file diff --git a/ui-tests/general.spec.js b/ui-tests/general.spec.js index 6d939aa..6833232 100644 --- a/ui-tests/general.spec.js +++ b/ui-tests/general.spec.js @@ -7,29 +7,32 @@ test.beforeEach(({ page }) => { errors = []; page.on('console', msg => { if (msg.type() === 'error') { - console.log(msg.text()); + errors.push(msg.text()); } }) + page.on("pageerror", (err) => { + errors.push(err.message); + }) }); test.afterEach(({ page }) => { - expect(errors).toEqual([]); + expect(errors).toEqual([]); }); test('network graph loaded', async ({ page }) => { - await page.goto('http://127.0.0.1:8080/'); + await page.goto('http://127.0.0.1:8080/'); - await expect(page).toHaveTitle('HIV-TRACE'); - await expect(page.locator("#hiv-trace-network-svg")).toBeVisible(); + await expect(page).toHaveTitle('HIV-TRACE'); + await expect(page.locator("#hiv-trace-network-svg")).toBeVisible(); }); test('network statistics loaded', async ({ page }) => { - await page.goto('http://127.0.0.1:8080/'); + await page.goto('http://127.0.0.1:8080/'); - await expect(page.locator("#graph-tab")).toBeVisible(); - await page.locator("#graph-tab").click(); - await expect(page.locator("#trace-graph")).toBeVisible(); - await expect(page.getByText("Sequences used to make links")).toBeVisible(); - await expect(page.getByText("0.891%")).toBeVisible(); + await expect(page.locator("#graph-tab")).toBeVisible(); + await page.locator("#graph-tab").click(); + await expect(page.locator("#trace-graph")).toBeVisible(); + await expect(page.getByText("Sequences used to make links")).toBeVisible(); + await expect(page.getByText("0.891%")).toBeVisible(); }); \ No newline at end of file diff --git a/ui-tests/tables.spec.js b/ui-tests/tables.spec.js index 2e1898c..56c59d3 100644 --- a/ui-tests/tables.spec.js +++ b/ui-tests/tables.spec.js @@ -6,13 +6,15 @@ test.beforeEach(({ page }) => { errors = []; page.on('console', msg => { if (msg.type() === 'error') { - console.log(msg.text()); errors.push(msg.text()); } }) + page.on("pageerror", (err) => { + errors.push(err.message); + }) }); -test.afterEach(({ page }) => { +test.afterEach(async ({ page }) => { expect(errors).toEqual([]); }); @@ -62,10 +64,66 @@ test('node table loads and works', async ({ page }) => { await expect(page.locator("#trace-nodes")).toBeVisible(); await expect(page.getByText("# of links", { exact: true })).toBeVisible(); - await expect(page.getByText("GA01HAGUS002656*")).toBeVisible(); + await expect(page.getByText("GA01HAGUS002656*")).toBeVisible(); await page.getByText("view cluster").first().click(); await expect(page.getByText("Cluster 1")).toBeVisible(); await expect(await page.locator("#hivtrace-export-image").count()).toBe(2); await expect(page.locator("#hivtrace-export-image").last()).toBeVisible(); +}) + +test('node table sorting and filtering works', async ({ page }) => { + // sorting + await page.goto('http://localhost:8080/html/priority-sets-args.html?network=../ui-tests/data/network.json'); + await expect(page.locator("#nodes-tab")).toBeVisible(); + await page.locator("#nodes-tab").click(); + + await page.locator("#hivtrace_node_column_hiv_aids_dx_dt_3").click(); + await page.locator("[aria-labelledby='hivtrace_node_column_hiv_aids_dx_dt_3']").locator("li a").getByText("age_dx").click(); + await page.locator("#node_table").locator("tbody tr").nth(3) + .filter({ has: page.getByText("BMK384244US2007") }) + .filter({ has: page.getByText("87") }) + .filter({ has: page.getByText("Asian") }) + + await page.locator("[title='age_dx']").locator("[data-column-id='3']").click(); + await page.locator("#node_table").locator("tbody tr").nth(3) + .filter({ has: page.getByText("BMK385331US2015") }) + .filter({ has: page.getByText("13-19") }) + .filter({ has: page.getByText("American Indian/Alaska Native") }) + + await page.locator("[title='age_dx']").locator("[data-column-id='3']").click(); + await expect(page.locator("#trace-nodes").locator(".fa-sort-amount-desc")).toHaveCount(1); + await page.locator("#node_table").locator("tbody tr").nth(3) + .filter({ has: page.getByText("BMN090524US2018") }) + .filter({ has: page.getByText("60") }) + .filter({ has: page.getByText("Hispanic/Latino") }) + + await page.locator("#hivtrace_node_column_age_dx_3").click(); + await page.locator("[aria-labelledby='hivtrace_node_column_age_dx_3']").locator("li a").getByText("hiv_aids_dx_dt").click(); + await page.locator("[title='Node ID']").locator("[data-column-id='0']").click(); + await page.locator("#node_table").locator("tbody tr").nth(3) + .filter({ has: page.getByText("-MN467202US2016") }) + .filter({ has: page.getByText("Heterosexual Contact-Male") }) + .filter({ has: page.getByText("2022-04-15") }) + + await expect(page.locator("#trace-nodes").locator(".fa-sort-amount-desc")).toHaveCount(0); + await expect(page.locator("#trace-nodes").locator(".fa-sort-amount-asc")).toHaveCount(1); + + // filtering + await page.locator("#trace-nodes thead").locator("[title='hiv_aids_dx_dt']").locator("a").filter({ + has: page.locator(".fa-search") + }).click(); + // wait because fill doesn't work immediately + await page.waitForTimeout(500); + await page.locator("[data-hivtrace-ui-role='table-filter-term']").fill("20220101:20220201"); + await page.waitForTimeout(500); + await page.keyboard.press("Enter"); + await expect(page.locator("#node_table").locator("tbody tr").locator('visible=true').filter({ + has: page.getByText("2022-04-15") + })).toHaveCount(0); + + await page.locator('[data-hivtrace-ui-role="table-filter-reset"]').click(); + await expect(await page.locator("#node_table").locator("tbody tr").locator('visible=true').filter({ + has: page.getByText("2022-04-15") + }).count()).toBeGreaterThan(0); }) \ No newline at end of file