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