diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..75bf826 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.tabSize": 2, + "editor.insertSpaces": true, +} \ No newline at end of file diff --git a/src/clusternetwork.js b/src/clusternetwork.js index b6e4cbc..8158843 100755 --- a/src/clusternetwork.js +++ b/src/clusternetwork.js @@ -1,14 +1,18 @@ var d3 = require("d3"), _ = require("underscore"), jsConvert = require("js-convert-case"), - misc = require("./misc"), - helpers = require("./helpers"), - colorPicker = require("./colorPicker"), - scatterPlot = require("./scatterplot"), topojson = require("topojson"), jsPanel = require("jspanel4").jsPanel, autocomplete = require("autocomplete.js"); +var misc = require("./misc"), + helpers = require("./helpers"), + colorPicker = require("./colorPicker"), + scatterPlot = require("./scatterplot"), + utils = require("./utils"), + tables = require("./tables"); + + const _networkSubclusterSeparator = "."; const _networkNewNodeMarker = "[+]"; var _networkGraphAttrbuteID = "patient_attribute_schema"; @@ -685,18 +689,6 @@ var hivtrace_cluster_network_graph = function ( return d; }; - self.get_ui_element_selector_by_role = function (role, not_nested) { - if (not_nested && !self.primary_graph) { - return undefined; - } - return ( - (not_nested ? "" : "#" + self.ui_container_selector) + - " [data-hivtrace-ui-role='" + - role + - "']" - ); - }; - if (json.Settings && json.Settings.created) { self.today = new Date(json.Settings.created); } else { @@ -882,7 +874,7 @@ var hivtrace_cluster_network_graph = function ( "#" + clicked_object.attr("aria-describedby") ); var list_element = popover_div.selectAll( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "priority-membership-list", true ) @@ -1327,7 +1319,7 @@ var hivtrace_cluster_network_graph = function ( self.display_warning(self.warning_string, true); } - let tab_pill = self.get_ui_element_selector_by_role( + let tab_pill = utils.get_ui_element_selector_by_role( "priority_set_counts", true ); @@ -2415,13 +2407,13 @@ var hivtrace_cluster_network_graph = function ( "#" + clicked_object.attr("aria-describedby") ); var textarea_element = popover_div.selectAll( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "priority-description-form", true ) ); var button_element = popover_div.selectAll( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "priority-description-save", true ) @@ -2434,7 +2426,7 @@ var hivtrace_cluster_network_graph = function ( this_button.click(); }); button_element = popover_div.selectAll( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "priority-description-dismiss", true ) @@ -2739,7 +2731,7 @@ var hivtrace_cluster_network_graph = function ( (k) => $( d3 - .select(self.get_ui_element_selector_by_role(k, true)) + .select(utils.get_ui_element_selector_by_role(k, true)) .node() ).val() ); @@ -2813,7 +2805,7 @@ var hivtrace_cluster_network_graph = function ( panel_object.close(); if (validation_mode == "validate") { if (self.priority_set_table_writeable) { - let tab_pill = self.get_ui_element_selector_by_role( + let tab_pill = utils.get_ui_element_selector_by_role( "priority_set_counts", true ), @@ -2827,7 +2819,7 @@ var hivtrace_cluster_network_graph = function ( panel_object.first_save = false; } let panel_to_focus = document.querySelector( - self.get_ui_element_selector_by_role("priority-panel-name", true) + utils.get_ui_element_selector_by_role("priority-panel-name", true) ); if (panel_to_focus) panel_to_focus.focus(); return res; @@ -2905,7 +2897,7 @@ var hivtrace_cluster_network_graph = function ( } var auto_object = autocomplete( - self.get_ui_element_selector_by_role("priority-panel-nodeids", true), + utils.get_ui_element_selector_by_role("priority-panel-nodeids", true), { hint: false }, [ { @@ -3344,7 +3336,7 @@ var hivtrace_cluster_network_graph = function ( }; if (self._is_CDC_ && self.primary_graph) { - let new_set = self.get_ui_element_selector_by_role( + let new_set = utils.get_ui_element_selector_by_role( "new_priority_set", true ); @@ -3364,7 +3356,7 @@ var hivtrace_cluster_network_graph = function ( }); } - let merge_sets = self.get_ui_element_selector_by_role( + let merge_sets = utils.get_ui_element_selector_by_role( "merge_priority_sets", true ); @@ -3372,7 +3364,7 @@ var hivtrace_cluster_network_graph = function ( if (merge_sets) { d3.selectAll(merge_sets).on("click", function (e) { $( - self.get_ui_element_selector_by_role("priority_set_merge", true) + utils.get_ui_element_selector_by_role("priority_set_merge", true) ).modal(); }); } @@ -5688,7 +5680,7 @@ var hivtrace_cluster_network_graph = function ( } d3.select( - self.get_ui_element_selector_by_role("cluster_list_data_export", true) + utils.get_ui_element_selector_by_role("cluster_list_data_export", true) ).on("click", function (d) { if (self._is_CDC_executive_mode) { alert(_networkWarnExecutiveMode); @@ -5744,7 +5736,7 @@ var hivtrace_cluster_network_graph = function ( self._setup_cluster_list_view = function () { d3.select( - self.get_ui_element_selector_by_role("cluster_list_view_toggle", true) + utils.get_ui_element_selector_by_role("cluster_list_view_toggle", true) ).on("click", function () { d3.event.preventDefault(); var group_by_id = false; @@ -5766,13 +5758,13 @@ var hivtrace_cluster_network_graph = function ( cluster_id ? cluster_id.toString() : "", !group_by_id, d3.select( - self.get_ui_element_selector_by_role("cluster_list_payload", true) + utils.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( + $(utils.get_ui_element_selector_by_role("cluster_list", true)).on( "show.bs.modal", function (event) { var link_clicked = $(event.relatedTarget); @@ -5780,7 +5772,7 @@ var hivtrace_cluster_network_graph = function ( var priority_list = link_clicked.data("priority_set"); var modal = d3.select( - self.get_ui_element_selector_by_role("cluster_list", true) + utils.get_ui_element_selector_by_role("cluster_list", true) ); modal .selectAll(".modal-title") @@ -5792,7 +5784,7 @@ var hivtrace_cluster_network_graph = function ( ); var view_toggle = $( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "cluster_list_view_toggle", true ) @@ -5810,27 +5802,27 @@ var hivtrace_cluster_network_graph = function ( cluster_id, //cluster_id, $( - self.get_ui_element_selector_by_role( + utils.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) + utils.get_ui_element_selector_by_role("cluster_list_payload", true) ), priority_list ); } ); - $(self.get_ui_element_selector_by_role("overlap_list", true)).on( + $(utils.get_ui_element_selector_by_role("overlap_list", true)).on( "show.bs.modal", function (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) + utils.get_ui_element_selector_by_role("overlap_list", true) ); modal .selectAll(".modal-title") @@ -5875,7 +5867,7 @@ var hivtrace_cluster_network_graph = function ( }); d3.select( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "overlap_list_data_export", true ) @@ -5883,9 +5875,9 @@ var hivtrace_cluster_network_graph = function ( helpers.export_csv_button(rows_for_export, "overlap"); }); - add_a_sortable_table( + tables.add_a_sortable_table( modal.select( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "overlap_list_data_table", true ) @@ -5893,23 +5885,24 @@ var hivtrace_cluster_network_graph = function ( headers, rows, true, - null + null, + self.has_priority_set_editor() ); } ); }; - $(self.get_ui_element_selector_by_role("priority_set_merge", true)).on( + $(utils.get_ui_element_selector_by_role("priority_set_merge", true)).on( "show.bs.modal", function (event) { var modal = d3.select( - self.get_ui_element_selector_by_role("priority_set_merge", true) + utils.get_ui_element_selector_by_role("priority_set_merge", true) ); let desc = modal.selectAll(".modal-desc"); let proceed_btn = d3.select( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "priority_set_merge_table_proceed", true ) @@ -6037,9 +6030,9 @@ var hivtrace_cluster_network_graph = function ( ]); }); - add_a_sortable_table( + tables.add_a_sortable_table( modal.select( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "priority_set_merge_table", true ) @@ -6047,7 +6040,8 @@ var hivtrace_cluster_network_graph = function ( headers, rows, true, - null + null, + self.has_priority_set_editor() ); } } @@ -6057,7 +6051,7 @@ var hivtrace_cluster_network_graph = function ( self._setup_cluster_list_view(); var cluster_ui_container = d3.select( - self.get_ui_element_selector_by_role("cluster_operations_container") + utils.get_ui_element_selector_by_role("cluster_operations_container") ); cluster_ui_container.selectAll("li").remove(); @@ -6320,7 +6314,7 @@ var hivtrace_cluster_network_graph = function ( }, cluster_ui_container); var button_group = d3.select( - self.get_ui_element_selector_by_role("button_group") + utils.get_ui_element_selector_by_role("button_group") ); if (!button_group.empty()) { @@ -6387,7 +6381,7 @@ var hivtrace_cluster_network_graph = function ( } var export_image = d3.select( - self.get_ui_element_selector_by_role("export_image") + utils.get_ui_element_selector_by_role("export_image") ); if (!export_image.empty()) { @@ -6436,7 +6430,7 @@ var hivtrace_cluster_network_graph = function ( } } - $(self.get_ui_element_selector_by_role("filter")) + $(utils.get_ui_element_selector_by_role("filter")) .off("input propertychange") .on( "input propertychange", @@ -6446,7 +6440,7 @@ var hivtrace_cluster_network_graph = function ( }, 250) ); - $(self.get_ui_element_selector_by_role("hide_filter")) + $(utils.get_ui_element_selector_by_role("hide_filter")) .off("change") .on( "change", @@ -6457,7 +6451,7 @@ var hivtrace_cluster_network_graph = function ( }, 250) ); - $(self.get_ui_element_selector_by_role("show_small_clusters")) + $(utils.get_ui_element_selector_by_role("show_small_clusters")) .off("change") .on( "change", @@ -6472,7 +6466,7 @@ var hivtrace_cluster_network_graph = function ( }, 250) ); - $(self.get_ui_element_selector_by_role("set_min_cluster_size")) + $(utils.get_ui_element_selector_by_role("set_min_cluster_size")) .off("change") .on( "change", @@ -6482,7 +6476,7 @@ var hivtrace_cluster_network_graph = function ( }, 250) ); - $(self.get_ui_element_selector_by_role("pairwise_table_pecentage", true)) + $(utils.get_ui_element_selector_by_role("pairwise_table_pecentage", true)) .off("change") .on( "change", @@ -6557,11 +6551,11 @@ var hivtrace_cluster_network_graph = function ( try { if (options && options["extra_menu"]) { var extra_ui_container = d3.select( - self.get_ui_element_selector_by_role("extra_operations_container") + utils.get_ui_element_selector_by_role("extra_operations_container") ); d3.select( - self.get_ui_element_selector_by_role("extra_operations_enclosure") + utils.get_ui_element_selector_by_role("extra_operations_enclosure") ) .selectAll("button") .text(options["extra_menu"]["title"]) @@ -6588,7 +6582,7 @@ var hivtrace_cluster_network_graph = function ( }, extra_ui_container); d3.select( - self.get_ui_element_selector_by_role("extra_operations_enclosure") + utils.get_ui_element_selector_by_role("extra_operations_enclosure") ).style("display", null); } } catch (err) { @@ -6756,9 +6750,9 @@ var hivtrace_cluster_network_graph = function ( //valid_cats.splice (0,0, {'label' : 'None', 'index' : -1}); [ - d3.select(self.get_ui_element_selector_by_role("attributes")), + d3.select(utils.get_ui_element_selector_by_role("attributes")), d3.select( - self.get_ui_element_selector_by_role("attributes_cat", true) + utils.get_ui_element_selector_by_role("attributes_cat", true) ), ].forEach(function (m) { //console.log (m); @@ -6863,7 +6857,7 @@ var hivtrace_cluster_network_graph = function ( }); }); - [d3.select(self.get_ui_element_selector_by_role("shapes"))].forEach( + [d3.select(utils.get_ui_element_selector_by_role("shapes"))].forEach( function (m) { m.selectAll("li").remove(); var cat_menu = m.selectAll("li").data( @@ -6938,7 +6932,7 @@ var hivtrace_cluster_network_graph = function ( } ); - $(self.get_ui_element_selector_by_role("opacity_invert")) + $(utils.get_ui_element_selector_by_role("opacity_invert")) .off("click") .on("click", function (e) { if (self.colorizer["opacity_scale"]) { @@ -6951,7 +6945,7 @@ var hivtrace_cluster_network_graph = function ( $(this).toggleClass("btn-active btn-default"); }); - $(self.get_ui_element_selector_by_role("attributes_invert")) + $(utils.get_ui_element_selector_by_role("attributes_invert")) .off("click") .on("click", function (e) { if (self.colorizer["category_id"]) { @@ -6976,7 +6970,7 @@ var hivtrace_cluster_network_graph = function ( $(this).toggleClass("btn-active btn-default"); }); - [d3.select(self.get_ui_element_selector_by_role("opacity"))].forEach( + [d3.select(utils.get_ui_element_selector_by_role("opacity"))].forEach( function (m) { m.selectAll("li").remove(); var cat_menu = m.selectAll("li").data( @@ -7178,22 +7172,6 @@ var hivtrace_cluster_network_graph = function ( self.update(); } - function sort_table_toggle_icon(element, value) { - //console.log (value); - if (value) { - $(element).data("sorted", value); - d3.select(element) - .selectAll("i") - .classed("fa-sort-amount-desc", value == "desc") - .classed("fa-sort-amount-asc", value == "asc") - .classed("fa-sort", value == "unsorted"); - } else { - var sorted_state = $(element).data("sorted"); - sort_table_toggle_icon(element, sorted_state == "asc" ? "desc" : "asc"); - return sorted_state == "asc" ? d3.descending : d3.ascending; - } - } - /** element is the sortable clicker **/ function filter_table_by_column_handler(datum, conditions) { @@ -7274,7 +7252,7 @@ var hivtrace_cluster_network_graph = function ( }); d3.select(table_element[0]) .select("caption") - .select(self.get_ui_element_selector_by_role("table-count-shown", true)) + .select(utils.get_ui_element_selector_by_role("table-count-shown", true)) .text(shown_rows); /*.selectAll("td").each (function (d, i) { @@ -7297,437 +7275,6 @@ var hivtrace_cluster_network_graph = function ( } } - /** element is the sortable clicker **/ - function sort_table_by_column(element, datum) { - if (d3.event) { - d3.event.preventDefault(); - } - var table_element = $(element).closest("table"); - if (table_element.length) { - var sort_on = parseInt($(element).data("column-id")); - var sort_key = datum.sort; - - var sorted_state = $(element).data("sorted"); - var sorted_function = sort_table_toggle_icon(element); - - var sort_accessor; - - if (sort_key) { - if (_.isFunction(sort_key)) { - sort_accessor = function (x) { - return sort_key(x); - }; - } else { - sort_accessor = function (x) { - var val = x[sort_key]; - if (_.isFunction(val)) return val(); - return val; - }; - } - } else { - sort_accessor = function (x) { - return x; - }; - } - - d3.select(table_element[0]) - .select("tbody") - .selectAll("tr") - .sort(function (a, b) { - return sorted_function( - sort_accessor(a[sort_on]), - sort_accessor(b[sort_on]) - ); - }); - - // select all other elements from thead and toggle their icons - - $(table_element) - .find("thead [data-column-id]") - .filter(function () { - return parseInt($(this).data("column-id")) != sort_on; - }) - .each(function () { - sort_table_toggle_icon(this, "unsorted"); - }); - } - } - - function table_get_cell_value(data) { - return _.isFunction(data.value) ? data.value() : data.value; - } - - function format_a_cell(data, index, item) { - var this_sel = d3.select(item); - var current_value = table_get_cell_value(data); - var handle_sort = this_sel; - - handle_sort.selectAll("*").remove(); - - if ("callback" in data) { - handle_sort = data.callback(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); - else this_sel.text(repr); - } - - if ("filter" in data) { - data.filter_term = ""; - data.column_id = index; - - if (data.value == _networkNodeIDField) { - // this is an ugly hardcode. - let pse = self.has_priority_set_editor(); - if (pse) { - //console.log ("Here"); - var add_to_ps = handle_sort.append("a").property("href", "#"); - add_to_ps - .append("i") - .classed("fa fa-plus-square fa-lg", true) - .style("margin-left", "0.2em") - .attr( - "title", - "Add currently visible nodes to the Cluster of Interest" - ); - - add_to_ps.on("click", function (d) { - let node_ids = []; - self.node_table.selectAll("tr").each(function (d, i) { - let this_row = d3.select(this); - if (this_row.style("display") != "none") { - this_row.selectAll("td").each(function (d, j) { - if (j == data.column_id) { - let has_marker = d.value.indexOf(_networkNewNodeMarker); - if (has_marker > 0) { - node_ids.push(d.value.substring(0, has_marker)); - } else { - node_ids.push(d.value); - } - } - }); - } - }); - pse.append_nodes(node_ids); - }); - } - } - - var clicker = handle_sort.append("a").property("href", "#"); - - clicker - .append("i") - .classed("fa fa-search", true) - .style("margin-left", "0.2em"); - - var search_form_generator = function () { - return ( - '
\ -
\ -
\ - \ -
\ -
\ -
\ - \ -
\ -
\ -
\ -
\ -
\ -
\ - Type in text to select columns which \ - contain the term.
\ - For example, typing in MSM will select rows\ - that have "MSM" as a part of the column value.\ -

\ - Type in space separated terms (MSM IDU) to\ - search for either term.

\ - Type in terms in quotes ("male") to search\ - for this exact term.

\ - If columns have date information you can use\ - YYYYMMDD:YYYYMMDD to search for date ranges.

\ - Use <value or >value\ - to search numerical columns

\ -

\ -
\ - ' - ); - }; - - var search_popover = $(clicker.node()) - .popover({ - html: true, - sanitize: false, - content: search_form_generator, - placement: "bottom", - }) - .on("shown.bs.popover", function (e) { - var search_icon = d3.select(this); - - const update_term = function (v) { - data.filter_term = v; - search_icon - .selectAll("i") - .classed("fa-search", !v.length) - .classed("fa-search-plus", v.length); - }; - - var popover_div = d3.select( - "#" + d3.select(this).attr("aria-describedby") - ); - var search_click = popover_div.selectAll( - self.get_ui_element_selector_by_role("table-filter-apply", true) - ); - var reset_click = popover_div.selectAll( - self.get_ui_element_selector_by_role("table-filter-reset", true) - ); - var search_box = popover_div.selectAll( - self.get_ui_element_selector_by_role("table-filter-term", true) - ); - - search_box.property("value", data.filter_term); - - search_click.on("click", function (d) { - update_term(search_box.property("value")); - filter_table(clicker.node()); - }); - - reset_click.on("click", function (d) { - search_box.property("value", ""); - update_term(""); - filter_table(clicker.node()); - }); - }); - } - - if (handle_sort && "sort" in data) { - var clicker = handle_sort - .append("a") - .property("href", "#") - .on("click", function (d) { - sort_table_by_column(this, d); - }) - .attr("data-sorted", "unsorted") - .attr("data-column-id", index); - clicker - .append("i") - .classed("fa fa-sort", true) - .style("margin-left", "0.2em"); - - if ("presort" in data) { - if (data["presort"] == "desc") { - clicker.attr("data-sorted", "asc"); - } - sort_table_by_column(clicker.node(), data); - } - } - - if ("actions" in data) { - let by_group = data.actions; - - if (!(_.isArray(data.actions) && _.isArray(data.actions[0]))) { - by_group = [data.actions]; - } - - _.each(by_group, (bgrp) => { - let button_group = handle_sort - .append("div") - .classed("btn-group btn-group-xs", true) - .attr("style", "padding-left:0.5em"); - _.each( - _.isFunction(bgrp) ? bgrp(button_group, current_value) : bgrp, - (b) => { - if (_.isFunction(b)) { - b = b(button_group, current_value); - } - if (b) { - let this_button = null; - if (_.isArray(b.dropdown)) { - let button_group_dropdown = button_group - .append("div") - .classed("btn-group btn-group-xs", true); - - this_button = button_group_dropdown - .append("button") - .classed("btn btn-default btn-xs dropdown-toggle", true) - .attr("data-toggle", "dropdown"); - - var dropdown_list = button_group_dropdown - .append("ul") - .classed("dropdown-menu", true); - //.attr("aria-labelledby", menu_id); - - let is_option_array = _.isObject(b.dropdown[0]); - let items = b.dropdown; - - function get_item_text(item) { - if (_.has(item, "label")) { - return item["label"]; - } - return item; - } - - dropdown_list = dropdown_list.selectAll("li").data(items); - dropdown_list.enter().append("li"); - dropdown_list.each(function (data, i) { - var handle_change = d3 - .select(this) - .append("a") - .attr("href", "#") - .text(function (data) { - return get_item_text(data); - }); - if (_.has(data, "data") && data["data"]) { - //let element = $(this_button.node()); - _.each(data.data, (v, k) => { - handle_change.attr("data-" + k, v); - }); - } - handle_change.on("click", function (d) { - if (_.has(d, "action") && d["action"]) { - d["action"](this_button, d["label"]); - } else { - if (b.action) b.action(this_button, get_item_text(d)); - } - }); - }); - } else { - this_button = button_group - .append("button") - .classed("btn btn-default btn-xs", true); - if (b.action) - this_button.on("click", function (e) { - d3.event.preventDefault(); - b.action(this_button, current_value); - }); - } - if (b.icon) { - this_button.append("i").classed("fa " + b.icon, true); - } else { - this_button.text(b.text).style("font-size", "12px"); - } - - if (b.data) { - //let element = $(this_button.node()); - _.each(b.data, (v, k) => { - this_button.attr("data-" + k, v); - }); - } - - if (b.classed) { - _.each(b.classed, (v, k) => { - this_button.classed(k, v); - }); - } - - if (b.help) { - this_button.attr("title", b.help); - } - } - } - ); - }); - } - - if ("help" in data) { - this_sel.attr("title", data.help); - } - } - - function add_a_sortable_table( - container, - headers, - content, - overwrite, - caption - ) { - if (!container || !container.node()) { - return; - } - - var thead = container.selectAll("thead"); - var tbody = container.selectAll("tbody"); - - const set_table_elements = (d, cell) => { - if (d.width || d.text_wrap) { - cell = d3.select(cell); - if (d.width) cell.style("width", "" + d.width + "px"); - if (d.text_wrap) { - cell - .style("overflow", "hidden") - .style("white-space", "nowrap") - .style("text-overflow", "ellipsis"); - } - } - }; - - if (tbody.empty() || overwrite) { - tbody.remove(); - tbody = d3.select(document.createElement("tbody")); - tbody - .selectAll("tr") - .data(content) - .enter() - .append("tr") - .selectAll("td") - .data(function (d) { - return d; - }) - .enter() - .append("td") - .call(function (selection) { - return selection.each(function (d, i) { - set_table_elements(d, this); - format_a_cell(d, i, this); - }); - }); - container.node().appendChild(tbody.node()); - } - - // head AFTER rows, so we can handle pre-sorting - if (thead.empty() || overwrite) { - thead.remove(); - thead = container.insert("thead", ":first-child"); - thead - .selectAll("tr") - .data(headers) - .enter() - .append("tr") - .selectAll("th") - .data(function (d) { - return d; - }) - .enter() - .append("th") - .call(function (selection) { - return selection.each(function (d, i) { - set_table_elements(d, this); - format_a_cell(d, i, this); - }); - }); - } - //'Showing --/-- network nodes'); - - if (caption) { - var table_caption = container.selectAll("caption").data([caption]); - table_caption.enter().insert("caption", ":first-child"); - table_caption.html(function (d) { - return d; - }); - table_caption - .select(self.get_ui_element_selector_by_role("table-count-total", true)) - .text(content.length); - table_caption - .select(self.get_ui_element_selector_by_role("table-count-shown", true)) - .text(content.length); - } - } - function _cluster_table_draw_id(element, payload) { var this_cell = d3.select(element); this_cell.selectAll("*").remove(); @@ -7779,7 +7326,7 @@ var hivtrace_cluster_network_graph = function ( .attr("data-toggle", "modal") .attr( "data-target", - self.get_ui_element_selector_by_role("cluster_list", true) + utils.get_ui_element_selector_by_role("cluster_list", true) ) .attr("data-cluster", cluster_id) .append("i") @@ -8083,7 +7630,7 @@ var hivtrace_cluster_network_graph = function ( return "volatile" in d; }) .each(function (d, i) { - format_a_cell(d, i, this); + tables.format_a_cell(d, i, this); }); }; @@ -8569,7 +8116,7 @@ var hivtrace_cluster_network_graph = function ( label: "List overlaps", data: { toggle: "modal", - target: self.get_ui_element_selector_by_role( + target: utils.get_ui_element_selector_by_role( "overlap_list", true ), @@ -8685,7 +8232,7 @@ var hivtrace_cluster_network_graph = function ( label: "View nodes in this cluster of interest", data: { toggle: "modal", - target: self.get_ui_element_selector_by_role( + target: utils.get_ui_element_selector_by_role( "cluster_list", true ), @@ -8832,7 +8379,7 @@ var hivtrace_cluster_network_graph = function ( has_required_actions = ""; }*/ - add_a_sortable_table( + tables.add_a_sortable_table( container, headers, rows, @@ -8841,11 +8388,12 @@ var hivtrace_cluster_network_graph = function ( 'Showing --/-- clusters of interest.\ \ \ - ' + ', + self.has_priority_set_editor() ); d3.select( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "priority-subclusters-export", true ) @@ -8856,7 +8404,7 @@ var hivtrace_cluster_network_graph = function ( ); }); d3.select( - self.get_ui_element_selector_by_role( + utils.get_ui_element_selector_by_role( "priority-subclusters-export-csv", true ) @@ -9118,12 +8666,13 @@ var hivtrace_cluster_network_graph = function ( }); } - add_a_sortable_table( + tables.add_a_sortable_table( container, headers, rows, true, - table_caption + table_caption, + self.has_priority_set_editor() // rows ); } @@ -9373,12 +8922,13 @@ var hivtrace_cluster_network_graph = function ( } }); - add_a_sortable_table( + tables.add_a_sortable_table( element, headers, rows, true, - options && options["caption"] ? options["caption"] : null + options && options["caption"] ? options["caption"] : null, + self.has_priority_set_editor() ); } }; @@ -9641,7 +9191,7 @@ var hivtrace_cluster_network_graph = function ( var set_attr = "None"; ["shapes"].forEach(function (lbl) { - d3.select(self.get_ui_element_selector_by_role(lbl)) + d3.select(utils.get_ui_element_selector_by_role(lbl)) .selectAll("li") .selectAll("a") .attr("style", function (d, i) { @@ -9651,7 +9201,7 @@ var hivtrace_cluster_network_graph = function ( } return null; }); - d3.select(self.get_ui_element_selector_by_role(lbl + "_label")).html( + d3.select(utils.get_ui_element_selector_by_role(lbl + "_label")).html( __("network_tab")["shape"] + ": " + set_attr + @@ -10142,7 +9692,7 @@ var hivtrace_cluster_network_graph = function ( var set_attr = "None"; ["opacity"].forEach(function (lbl) { - d3.select(self.get_ui_element_selector_by_role(lbl)) + d3.select(utils.get_ui_element_selector_by_role(lbl)) .selectAll("li") .selectAll("a") .attr("style", function (d, i) { @@ -10152,7 +9702,7 @@ var hivtrace_cluster_network_graph = function ( } return null; }); - d3.select(self.get_ui_element_selector_by_role(lbl + "_label")).html( + d3.select(utils.get_ui_element_selector_by_role(lbl + "_label")).html( __("network_tab")["opacity"] + ": " + set_attr + @@ -10160,7 +9710,7 @@ var hivtrace_cluster_network_graph = function ( ); }); - d3.select(self.get_ui_element_selector_by_role("opacity_invert")) + d3.select(utils.get_ui_element_selector_by_role("opacity_invert")) .style("display", set_attr == "None" ? "none" : "inline") .classed("btn-active", false) .classed("btn-default", true); @@ -10205,7 +9755,7 @@ var hivtrace_cluster_network_graph = function ( ["attributes", false], ["attributes_cat", true], ].forEach(function (lbl) { - d3.select(self.get_ui_element_selector_by_role(lbl[0], lbl[1])) + d3.select(utils.get_ui_element_selector_by_role(lbl[0], lbl[1])) .selectAll("li") .selectAll("a") .attr("style", function (d, i) { @@ -10216,11 +9766,11 @@ var hivtrace_cluster_network_graph = function ( return null; }); d3.select( - self.get_ui_element_selector_by_role(lbl[0] + "_label", lbl[1]) + utils.get_ui_element_selector_by_role(lbl[0] + "_label", lbl[1]) ).html("Color: " + set_attr + ' '); }); - d3.select(self.get_ui_element_selector_by_role("attributes_invert")) + d3.select(utils.get_ui_element_selector_by_role("attributes_invert")) .style("display", set_attr == "None" ? "none" : "inline") .classed("btn-active", false) .classed("btn-default", true); @@ -10327,14 +9877,14 @@ var hivtrace_cluster_network_graph = function ( } }); d3.select( - self.get_ui_element_selector_by_role("aux_svg_holder_enclosed", true) + utils.get_ui_element_selector_by_role("aux_svg_holder_enclosed", true) ).style("display", null); scatterPlot.scatterPlot( points, 400, 400, - self.get_ui_element_selector_by_role("aux_svg_holder", true), + utils.get_ui_element_selector_by_role("aux_svg_holder", true), { x: "Source", y: "Target", @@ -10363,7 +9913,7 @@ var hivtrace_cluster_network_graph = function ( self.handle_attribute_categorical = function (cat_id, skip_update) { var set_attr = "None"; - d3.select(self.get_ui_element_selector_by_role("attributes_invert")).style( + d3.select(utils.get_ui_element_selector_by_role("attributes_invert")).style( "display", "none" ); @@ -10374,7 +9924,7 @@ var hivtrace_cluster_network_graph = function ( ["attributes", false], ["attributes_cat", true], ].forEach(function (lbl) { - d3.select(self.get_ui_element_selector_by_role(lbl[0], lbl[1])) + d3.select(utils.get_ui_element_selector_by_role(lbl[0], lbl[1])) .selectAll("li") .selectAll("a") .attr("style", function (d, i) { @@ -10385,7 +9935,7 @@ var hivtrace_cluster_network_graph = function ( return null; }); d3.select( - self.get_ui_element_selector_by_role(lbl[0] + "_label", lbl[1]) + utils.get_ui_element_selector_by_role(lbl[0] + "_label", lbl[1]) ).html("Color: " + set_attr + ' '); }); @@ -11456,7 +11006,7 @@ var hivtrace_cluster_network_graph = function ( } function render_binned_table(id, the_map, matrix) { - var the_table = d3.select(self.get_ui_element_selector_by_role(id, true)); + var the_table = d3.select(utils.get_ui_element_selector_by_role(id, true)); if (the_table.empty()) { return; } @@ -11465,7 +11015,7 @@ var hivtrace_cluster_network_graph = function ( the_table.selectAll("tbody").remove(); d3.select( - self.get_ui_element_selector_by_role(id + "_enclosed", true) + utils.get_ui_element_selector_by_role(id + "_enclosed", true) ).style("display", matrix ? null : "none"); if (matrix) { @@ -11556,7 +11106,7 @@ var hivtrace_cluster_network_graph = function ( } function render_chord_diagram(id, the_map, matrix) { - var container = d3.select(self.get_ui_element_selector_by_role(id, true)); + var container = d3.select(utils.get_ui_element_selector_by_role(id, true)); if (container.empty()) { return; @@ -11565,7 +11115,7 @@ var hivtrace_cluster_network_graph = function ( container.selectAll("svg").remove(); d3.select( - self.get_ui_element_selector_by_role(id + "_enclosed", true) + utils.get_ui_element_selector_by_role(id + "_enclosed", true) ).style("display", matrix ? null : "none"); if (matrix) { diff --git a/src/hivtrace.js b/src/hivtrace.js index b2ce6a5..a486cfe 100644 --- a/src/hivtrace.js +++ b/src/hivtrace.js @@ -4,6 +4,7 @@ import { hivtrace_cluster_graph_summary } from "./hivtrace_cluster_graph_summary import { histogram, histogramDistances } from "./histogram.js"; import { scatterPlot } from "./scatterplot.js"; +// QUESTION: should these be exported? (seems like they are not being used - misc, helpers, colorPicker) let misc = require("./misc.js"); let helpers = require("./helpers.js"); let colorPicker = require("./colorPicker.js"); diff --git a/src/tables.js b/src/tables.js new file mode 100644 index 0000000..02f16db --- /dev/null +++ b/src/tables.js @@ -0,0 +1,460 @@ +var d3 = require("d3"), + _ = require("underscore"), + utils = require("./utils.js"); + +var _networkNodeIDField = "hivtrace_node_id"; + +function add_a_sortable_table( + container, + headers, + content, + overwrite, + caption, + has_priority_set_editor +) { + if (!container || !container.node()) { + return; + } + + var thead = container.selectAll("thead"); + var tbody = container.selectAll("tbody"); + + const set_table_elements = (d, cell) => { + if (d.width || d.text_wrap) { + cell = d3.select(cell); + if (d.width) cell.style("width", "" + d.width + "px"); + if (d.text_wrap) { + cell + .style("overflow", "hidden") + .style("white-space", "nowrap") + .style("text-overflow", "ellipsis"); + } + } + }; + + if (tbody.empty() || overwrite) { + tbody.remove(); + tbody = d3.select(document.createElement("tbody")); + tbody + .selectAll("tr") + .data(content) + .enter() + .append("tr") + .selectAll("td") + .data(function (d) { + return d; + }) + .enter() + .append("td") + .call(function (selection) { + return selection.each(function (d, i) { + set_table_elements(d, this); + format_a_cell(d, i, this, has_priority_set_editor); + }); + }); + container.node().appendChild(tbody.node()); + } + + // head AFTER rows, so we can handle pre-sorting + if (thead.empty() || overwrite) { + thead.remove(); + thead = container.insert("thead", ":first-child"); + thead + .selectAll("tr") + .data(headers) + .enter() + .append("tr") + .selectAll("th") + .data(function (d) { + return d; + }) + .enter() + .append("th") + .call(function (selection) { + return selection.each(function (d, i) { + set_table_elements(d, this); + format_a_cell(d, i, this, has_priority_set_editor); + }); + }); + } + //'Showing --/-- network nodes'); + + if (caption) { + var table_caption = container.selectAll("caption").data([caption]); + table_caption.enter().insert("caption", ":first-child"); + table_caption.html(function (d) { + return d; + }); + table_caption + .select(utils.get_ui_element_selector_by_role("table-count-total", true)) + .text(content.length); + table_caption + .select(utils.get_ui_element_selector_by_role("table-count-shown", true)) + .text(content.length); + } +} + +function table_get_cell_value(data) { + return _.isFunction(data.value) ? data.value() : data.value; +} + +function format_a_cell(data, index, item, has_priority_set_editor) { + var this_sel = d3.select(item); + var current_value = table_get_cell_value(data); + var handle_sort = this_sel; + + handle_sort.selectAll("*").remove(); + + if ("callback" in data) { + handle_sort = data.callback(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); + else this_sel.text(repr); + } + + if ("filter" in data) { + data.filter_term = ""; + data.column_id = index; + + if (data.value == _networkNodeIDField) { + // this is an ugly hardcode. + if (has_priority_set_editor) { + //console.log ("Here"); + var add_to_ps = handle_sort.append("a").property("href", "#"); + add_to_ps + .append("i") + .classed("fa fa-plus-square fa-lg", true) + .style("margin-left", "0.2em") + .attr( + "title", + "Add currently visible nodes to the Cluster of Interest" + ); + + add_to_ps.on("click", function (d) { + let node_ids = []; + self.node_table.selectAll("tr").each(function (d, i) { + let this_row = d3.select(this); + if (this_row.style("display") != "none") { + this_row.selectAll("td").each(function (d, j) { + if (j == data.column_id) { + let has_marker = d.value.indexOf(_networkNewNodeMarker); + if (has_marker > 0) { + node_ids.push(d.value.substring(0, has_marker)); + } else { + node_ids.push(d.value); + } + } + }); + } + }); + pse.append_nodes(node_ids); + }); + } + } + + var clicker = handle_sort.append("a").property("href", "#"); + + clicker + .append("i") + .classed("fa fa-search", true) + .style("margin-left", "0.2em"); + + var search_form_generator = function () { + return ( + '
\ +
\ +
\ + \ +
\ +
\ +
\ + \ +
\ +
\ +
\ +
\ +
\ +
\ + Type in text to select columns which \ + contain the term.
\ + For example, typing in MSM will select rows\ + that have "MSM" as a part of the column value.\ +

\ + Type in space separated terms (MSM IDU) to\ + search for either term.

\ + Type in terms in quotes ("male") to search\ + for this exact term.

\ + If columns have date information you can use\ + YYYYMMDD:YYYYMMDD to search for date ranges.

\ + Use <value or >value\ + to search numerical columns

\ +

\ +
\ + ' + ); + }; + + var search_popover = $(clicker.node()) + .popover({ + html: true, + sanitize: false, + content: search_form_generator, + placement: "bottom", + }) + .on("shown.bs.popover", function (e) { + var search_icon = d3.select(this); + + const update_term = function (v) { + data.filter_term = v; + search_icon + .selectAll("i") + .classed("fa-search", !v.length) + .classed("fa-search-plus", v.length); + }; + + var popover_div = d3.select( + "#" + d3.select(this).attr("aria-describedby") + ); + var search_click = popover_div.selectAll( + utils.get_ui_element_selector_by_role("table-filter-apply", true) + ); + var reset_click = popover_div.selectAll( + utils.get_ui_element_selector_by_role("table-filter-reset", true) + ); + var search_box = popover_div.selectAll( + utils.get_ui_element_selector_by_role("table-filter-term", true) + ); + + search_box.property("value", data.filter_term); + + search_click.on("click", function (d) { + update_term(search_box.property("value")); + filter_table(clicker.node()); + }); + + reset_click.on("click", function (d) { + search_box.property("value", ""); + update_term(""); + filter_table(clicker.node()); + }); + }); + } + + if (handle_sort && "sort" in data) { + var clicker = handle_sort + .append("a") + .property("href", "#") + .on("click", function (d) { + sort_table_by_column(this, d); + }) + .attr("data-sorted", "unsorted") + .attr("data-column-id", index); + clicker + .append("i") + .classed("fa fa-sort", true) + .style("margin-left", "0.2em"); + + if ("presort" in data) { + if (data["presort"] == "desc") { + clicker.attr("data-sorted", "asc"); + } + sort_table_by_column(clicker.node(), data); + } + } + + if ("actions" in data) { + let by_group = data.actions; + + if (!(_.isArray(data.actions) && _.isArray(data.actions[0]))) { + by_group = [data.actions]; + } + + _.each(by_group, (bgrp) => { + let button_group = handle_sort + .append("div") + .classed("btn-group btn-group-xs", true) + .attr("style", "padding-left:0.5em"); + _.each( + _.isFunction(bgrp) ? bgrp(button_group, current_value) : bgrp, + (b) => { + if (_.isFunction(b)) { + b = b(button_group, current_value); + } + if (b) { + let this_button = null; + if (_.isArray(b.dropdown)) { + let button_group_dropdown = button_group + .append("div") + .classed("btn-group btn-group-xs", true); + + this_button = button_group_dropdown + .append("button") + .classed("btn btn-default btn-xs dropdown-toggle", true) + .attr("data-toggle", "dropdown"); + + var dropdown_list = button_group_dropdown + .append("ul") + .classed("dropdown-menu", true); + //.attr("aria-labelledby", menu_id); + + let is_option_array = _.isObject(b.dropdown[0]); + let items = b.dropdown; + + function get_item_text(item) { + if (_.has(item, "label")) { + return item["label"]; + } + return item; + } + + dropdown_list = dropdown_list.selectAll("li").data(items); + dropdown_list.enter().append("li"); + dropdown_list.each(function (data, i) { + var handle_change = d3 + .select(this) + .append("a") + .attr("href", "#") + .text(function (data) { + return get_item_text(data); + }); + if (_.has(data, "data") && data["data"]) { + //let element = $(this_button.node()); + _.each(data.data, (v, k) => { + handle_change.attr("data-" + k, v); + }); + } + handle_change.on("click", function (d) { + if (_.has(d, "action") && d["action"]) { + d["action"](this_button, d["label"]); + } else { + if (b.action) b.action(this_button, get_item_text(d)); + } + }); + }); + } else { + this_button = button_group + .append("button") + .classed("btn btn-default btn-xs", true); + if (b.action) + this_button.on("click", function (e) { + d3.event.preventDefault(); + b.action(this_button, current_value); + }); + } + if (b.icon) { + this_button.append("i").classed("fa " + b.icon, true); + } else { + this_button.text(b.text).style("font-size", "12px"); + } + + if (b.data) { + //let element = $(this_button.node()); + _.each(b.data, (v, k) => { + this_button.attr("data-" + k, v); + }); + } + + if (b.classed) { + _.each(b.classed, (v, k) => { + this_button.classed(k, v); + }); + } + + if (b.help) { + this_button.attr("title", b.help); + } + } + } + ); + }); + } + + if ("help" in data) { + this_sel.attr("title", data.help); + } +} + +/** element is the sortable clicker **/ +function sort_table_by_column(element, datum) { + if (d3.event) { + d3.event.preventDefault(); + } + var table_element = $(element).closest("table"); + if (table_element.length) { + var sort_on = parseInt($(element).data("column-id")); + var sort_key = datum.sort; + + var sorted_state = $(element).data("sorted"); + var sorted_function = sort_table_toggle_icon(element); + + var sort_accessor; + + if (sort_key) { + if (_.isFunction(sort_key)) { + sort_accessor = function (x) { + return sort_key(x); + }; + } else { + sort_accessor = function (x) { + var val = x[sort_key]; + if (_.isFunction(val)) return val(); + return val; + }; + } + } else { + sort_accessor = function (x) { + return x; + }; + } + + d3.select(table_element[0]) + .select("tbody") + .selectAll("tr") + .sort(function (a, b) { + return sorted_function( + sort_accessor(a[sort_on]), + sort_accessor(b[sort_on]) + ); + }); + + // select all other elements from thead and toggle their icons + + $(table_element) + .find("thead [data-column-id]") + .filter(function () { + return parseInt($(this).data("column-id")) != sort_on; + }) + .each(function () { + sort_table_toggle_icon(this, "unsorted"); + }); + } +} + +function sort_table_toggle_icon(element, value) { + //console.log (value); + if (value) { + $(element).data("sorted", value); + d3.select(element) + .selectAll("i") + .classed("fa-sort-amount-desc", value == "desc") + .classed("fa-sort-amount-asc", value == "asc") + .classed("fa-sort", value == "unsorted"); + } else { + var sorted_state = $(element).data("sorted"); + sort_table_toggle_icon(element, sorted_state == "asc" ? "desc" : "asc"); + return sorted_state == "asc" ? d3.descending : d3.ascending; + } +} + +module.exports = { + add_a_sortable_table, + format_a_cell, + sort_table_by_column, + sort_table_toggle_icon, +} + diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..6f9c311 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,16 @@ +// TODO: consolidate with other utility / helper function files +function get_ui_element_selector_by_role(role, not_nested) { + if (not_nested && !self.primary_graph) { + return undefined; + } + return ( + (not_nested ? "" : "#" + self.ui_container_selector) + + " [data-hivtrace-ui-role='" + + role + + "']" + ); + }; + +module.exports = { + get_ui_element_selector_by_role +}; \ No newline at end of file