From 1da50efbf184ac18bb549803543fea27cf65cbff Mon Sep 17 00:00:00 2001 From: Dave Eargle Date: Fri, 16 Jul 2021 14:54:02 -0600 Subject: [PATCH 1/3] add ability to d3 vis to live-update min-intersection requirement if `include_min_intersection_selector=True` is passed to mapper.visualize, then a number input is included at the top of the d3 vis which permits changing the min_intersection req for edges (links) to be drawn. Currently does not get saved in the downloadable config file. --- kmapper/kmapper.py | 8 ++- kmapper/static/kmapper.js | 92 +++++++++++++++++++++++++++++++--- kmapper/templates/toolbar.html | 11 ++++ kmapper/visuals.py | 3 +- test/test_visuals.py | 20 ++++++++ 5 files changed, 125 insertions(+), 9 deletions(-) diff --git a/kmapper/kmapper.py b/kmapper/kmapper.py index 09f2e8ed..ddf687e7 100644 --- a/kmapper/kmapper.py +++ b/kmapper/kmapper.py @@ -578,6 +578,7 @@ def map( "perc_overlap": self.cover.perc_overlap, "clusterer": str(clusterer), "scaler": str(self.scaler), + "nerve_min_intersection": nerve.min_intersection } graph["meta_nodes"] = meta @@ -639,6 +640,7 @@ def visualize( lens_names=None, nbins=10, include_searchbar=False, + include_min_intersection_selector=False ): """Generate a visualization of the simplicial complex mapper output. Turns the complex dictionary into a HTML/D3.js visualization @@ -729,6 +731,10 @@ def visualize( To reset any search-induced visual alterations, submit an empty search query. + include_min_intersection_selector: bool, default False + Whether to include an input to dynamically change the min_intersection + for an edge to be drawn. + Returns -------- html: string @@ -917,7 +923,7 @@ def visualize( ) html = _render_d3_vis( - title, mapper_summary, histogram, mapper_data, colorscale, include_searchbar + title, mapper_summary, histogram, mapper_data, colorscale, include_searchbar, include_min_intersection_selector ) if save_file: diff --git a/kmapper/static/kmapper.js b/kmapper/static/kmapper.js index a3b1fc50..52ebdee2 100644 --- a/kmapper/static/kmapper.js +++ b/kmapper/static/kmapper.js @@ -206,12 +206,6 @@ function set_histogram(selection, data){ var color_function_index = 0; var node_color_function_index = 0; -function reset_color_functions(){ - color_function_index = 0; - node_color_function_index = 0; - update_color_functions() -} - function update_color_functions(){ // update_meta_content_histogram set_histogram(d3.select('#meta_content .histogram'), summary_histogram[node_color_function_index][color_function_index]) @@ -282,7 +276,7 @@ function start() { simulation.force('link').links(links); simulation.alpha(1).restart() - reset_color_functions() + update_color_functions() } init(); @@ -620,6 +614,89 @@ d3.select('#searchbar') }) + +//https://stackoverflow.com/questions/51319147/map-default-value +class MapWithDefault extends Map { + get(key) { + if (!this.has(key)) { + this.set(key, this.default()) + }; + return super.get(key); + } + + constructor(defaultFunction, entries) { + super(entries); + this.default = defaultFunction; + } +} + +d3.select('#min_intersction_selector') + .on('submit', function(event){ + // replicates the logic in kmapper.nerve.GraphNerve.compute + event.preventDefault() + + let result = new MapWithDefault(() => []); + + // loop over all combinations of nodes + // https://stackoverflow.com/a/43241295/1396649 + let candidates = [] + let num_nodes = graph.nodes.length; + for (let i = 0; i < num_nodes - 1; i++){ + for (let j = i + 1; j < num_nodes; j++) { + candidates.push([i, j]); + } + } + + candidates.forEach(function(candidate) { + let node1_idx = candidate[0]; + let node2_idx = candidate[1]; + let node1 = graph.nodes[node1_idx]; + let node2 = graph.nodes[node2_idx]; + let intersection = node1.tooltip.custom_tooltips.filter(x => node2.tooltip.custom_tooltips.includes(x)); + if (intersection.length >= Number(min_intersction_selector_input.property('value')) ) { + result.get(node1_idx).push(node2_idx) + } + }) + + let edges = [] + result.forEach(function(value, key) { + let _edges = value.map(function(end) { + return [key, end] + }) + edges.push(_edges); + }) + + edges = edges.flat().map(function(edge) { + return { + 'source': edge[0], + 'target': edge[1], + 'width': 1 + } + }) + + graph.links = edges; + restart() + }) + + +// Dynamically size the min_intersection_selector input +let min_intersction_selector_input = d3.select('#min_intersction_selector input'); + +min_intersction_selector_input + .on('input', function(event){ + resizeInput.call(this) + }) + +function resizeInput() { + this.style.width = ( this.value.length + 3 ) + "ch"; +} + +// Only trigger if input is present +if (min_intersction_selector_input.size()) { + resizeInput.call(min_intersction_selector_input.node()) +} + + // Key press events let searchbar = d3.select('#searchbar input'); @@ -628,6 +705,7 @@ d3.select(window).on("keydown", function (event) { return; // Do nothing if the event was already processed } + // if searchbar is present and has focus if (searchbar.size() && searchbar.node().matches(':focus')){ return; // let them use the search bar. } diff --git a/kmapper/templates/toolbar.html b/kmapper/templates/toolbar.html index ad17e962..8199e694 100644 --- a/kmapper/templates/toolbar.html +++ b/kmapper/templates/toolbar.html @@ -65,4 +65,15 @@

Node Color Function {% endif %} + {% if include_min_intersection_selector %} +
+
+

Min Intersection Selector + + +

+
+
+ {% endif %} + diff --git a/kmapper/visuals.py b/kmapper/visuals.py index 8ef68595..197dc22e 100644 --- a/kmapper/visuals.py +++ b/kmapper/visuals.py @@ -548,7 +548,7 @@ def _format_tooltip( def _render_d3_vis( - title, mapper_summary, histogram, mapper_data, colorscale, include_searchbar + title, mapper_summary, histogram, mapper_data, colorscale, include_searchbar, include_min_intersection_selector ): # Find the module absolute path and locate templates module_root = os.path.join(os.path.dirname(__file__), "templates") @@ -587,6 +587,7 @@ def np_encoder(object, **kwargs): js_text=js_text, css_text=css_text, include_searchbar=include_searchbar, + include_min_intersection_selector=include_min_intersection_selector ) return html diff --git a/test/test_visuals.py b/test/test_visuals.py index f124a7dd..1dba64b6 100644 --- a/test/test_visuals.py +++ b/test/test_visuals.py @@ -393,6 +393,26 @@ def test_visualize_search_bar(self): include_searchbar=True, ) + def test_visualize_min_intersection_selector(self): + """ convenience test for generating a vis with a min_intersection_selector + (and also with multiple color_values _and_ multiple node_color_values)""" + mapper = KeplerMapper() + data, labels = make_circles(1000, random_state=0) + lens = mapper.fit_transform(data, projection=[0]) + graph = mapper.map(lens, data) + color_values = lens[:, 0] + + cv1 = np.array(lens) + cv2 = np.flip(cv1) + cv = np.column_stack([cv1, cv2]) + mapper.visualize( + graph, + color_values=cv, + node_color_function=["mean", "std"], + color_function_name=["hotdog", "hotdiggitydog"], + include_min_intersection_selector=True, + ) + def test_visualize_graph_with_cluster_stats_above_below(self): mapper = KeplerMapper() X = np.ones((1000, 3)) From f180368d30d9dedbfb09a45d189381a4ace56fb8 Mon Sep 17 00:00:00 2001 From: Dave Eargle Date: Fri, 16 Jul 2021 14:58:20 -0600 Subject: [PATCH 2/3] update release log --- RELEASE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index d6c1af50..252986ad 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,10 @@ # Release log +## 2.1.0 + +### Added +- ability to live-update the min-intersection threshold for edges on the d3 vis (#231) + ## 2.0.1 ### Fixed From c24cb3a8dd7e3aa67fbc0627d3652c96d2b1025a Mon Sep 17 00:00:00 2001 From: Dave Eargle Date: Tue, 5 Oct 2021 21:29:53 -0600 Subject: [PATCH 3/3] change to [unreleased] --- RELEASE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 252986ad..9319eb42 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,10 +1,12 @@ # Release log -## 2.1.0 + +## Unreleased ### Added - ability to live-update the min-intersection threshold for edges on the d3 vis (#231) + ## 2.0.1 ### Fixed