Skip to content

Commit e9d3d2b

Browse files
authored
add ability to d3 vis to live-update min-intersection requirement (#231)
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.
1 parent 7c3496a commit e9d3d2b

File tree

6 files changed

+132
-9
lines changed

6 files changed

+132
-9
lines changed

RELEASE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release log
22

3+
4+
## Unreleased
5+
6+
### Added
7+
- ability to live-update the min-intersection threshold for edges on the d3 vis (#231)
8+
9+
310
## 2.0.1
411

512
### Fixed

kmapper/kmapper.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ def map(
578578
"perc_overlap": self.cover.perc_overlap,
579579
"clusterer": str(clusterer),
580580
"scaler": str(self.scaler),
581+
"nerve_min_intersection": nerve.min_intersection
581582
}
582583
graph["meta_nodes"] = meta
583584

@@ -639,6 +640,7 @@ def visualize(
639640
lens_names=None,
640641
nbins=10,
641642
include_searchbar=False,
643+
include_min_intersection_selector=False
642644
):
643645
"""Generate a visualization of the simplicial complex mapper output. Turns the complex dictionary into a HTML/D3.js visualization
644646
@@ -729,6 +731,10 @@ def visualize(
729731
730732
To reset any search-induced visual alterations, submit an empty search query.
731733
734+
include_min_intersection_selector: bool, default False
735+
Whether to include an input to dynamically change the min_intersection
736+
for an edge to be drawn.
737+
732738
Returns
733739
--------
734740
html: string
@@ -917,7 +923,7 @@ def visualize(
917923
)
918924

919925
html = _render_d3_vis(
920-
title, mapper_summary, histogram, mapper_data, colorscale, include_searchbar
926+
title, mapper_summary, histogram, mapper_data, colorscale, include_searchbar, include_min_intersection_selector
921927
)
922928

923929
if save_file:

kmapper/static/kmapper.js

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,6 @@ function set_histogram(selection, data){
206206
var color_function_index = 0;
207207
var node_color_function_index = 0;
208208

209-
function reset_color_functions(){
210-
color_function_index = 0;
211-
node_color_function_index = 0;
212-
update_color_functions()
213-
}
214-
215209
function update_color_functions(){
216210
// update_meta_content_histogram
217211
set_histogram(d3.select('#meta_content .histogram'), summary_histogram[node_color_function_index][color_function_index])
@@ -282,7 +276,7 @@ function start() {
282276
simulation.force('link').links(links);
283277
simulation.alpha(1).restart()
284278

285-
reset_color_functions()
279+
update_color_functions()
286280
}
287281

288282
init();
@@ -620,6 +614,89 @@ d3.select('#searchbar')
620614

621615
})
622616

617+
618+
//https://stackoverflow.com/questions/51319147/map-default-value
619+
class MapWithDefault extends Map {
620+
get(key) {
621+
if (!this.has(key)) {
622+
this.set(key, this.default())
623+
};
624+
return super.get(key);
625+
}
626+
627+
constructor(defaultFunction, entries) {
628+
super(entries);
629+
this.default = defaultFunction;
630+
}
631+
}
632+
633+
d3.select('#min_intersction_selector')
634+
.on('submit', function(event){
635+
// replicates the logic in kmapper.nerve.GraphNerve.compute
636+
event.preventDefault()
637+
638+
let result = new MapWithDefault(() => []);
639+
640+
// loop over all combinations of nodes
641+
// https://stackoverflow.com/a/43241295/1396649
642+
let candidates = []
643+
let num_nodes = graph.nodes.length;
644+
for (let i = 0; i < num_nodes - 1; i++){
645+
for (let j = i + 1; j < num_nodes; j++) {
646+
candidates.push([i, j]);
647+
}
648+
}
649+
650+
candidates.forEach(function(candidate) {
651+
let node1_idx = candidate[0];
652+
let node2_idx = candidate[1];
653+
let node1 = graph.nodes[node1_idx];
654+
let node2 = graph.nodes[node2_idx];
655+
let intersection = node1.tooltip.custom_tooltips.filter(x => node2.tooltip.custom_tooltips.includes(x));
656+
if (intersection.length >= Number(min_intersction_selector_input.property('value')) ) {
657+
result.get(node1_idx).push(node2_idx)
658+
}
659+
})
660+
661+
let edges = []
662+
result.forEach(function(value, key) {
663+
let _edges = value.map(function(end) {
664+
return [key, end]
665+
})
666+
edges.push(_edges);
667+
})
668+
669+
edges = edges.flat().map(function(edge) {
670+
return {
671+
'source': edge[0],
672+
'target': edge[1],
673+
'width': 1
674+
}
675+
})
676+
677+
graph.links = edges;
678+
restart()
679+
})
680+
681+
682+
// Dynamically size the min_intersection_selector input
683+
let min_intersction_selector_input = d3.select('#min_intersction_selector input');
684+
685+
min_intersction_selector_input
686+
.on('input', function(event){
687+
resizeInput.call(this)
688+
})
689+
690+
function resizeInput() {
691+
this.style.width = ( this.value.length + 3 ) + "ch";
692+
}
693+
694+
// Only trigger if input is present
695+
if (min_intersction_selector_input.size()) {
696+
resizeInput.call(min_intersction_selector_input.node())
697+
}
698+
699+
623700
// Key press events
624701
let searchbar = d3.select('#searchbar input');
625702

@@ -628,6 +705,7 @@ d3.select(window).on("keydown", function (event) {
628705
return; // Do nothing if the event was already processed
629706
}
630707

708+
// if searchbar is present and has focus
631709
if (searchbar.size() && searchbar.node().matches(':focus')){
632710
return; // let them use the search bar.
633711
}

kmapper/templates/toolbar.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,15 @@ <h3>Node Color Function
6565
</div>
6666
{% endif %}
6767

68+
{% if include_min_intersection_selector %}
69+
<div id='min_intersction_selector' class="tool_item">
70+
<form>
71+
<h3>Min Intersection Selector
72+
<input type="number" min="1" value="{{ mapper_summary.custom_meta.nerve_min_intersection }}">
73+
<button class='btn' type='submit'>Update</button>
74+
</h3>
75+
</form>
76+
</div>
77+
{% endif %}
78+
6879
</div>

kmapper/visuals.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ def _format_tooltip(
550550

551551

552552
def _render_d3_vis(
553-
title, mapper_summary, histogram, mapper_data, colorscale, include_searchbar
553+
title, mapper_summary, histogram, mapper_data, colorscale, include_searchbar, include_min_intersection_selector
554554
):
555555
# Find the module absolute path and locate templates
556556
module_root = os.path.join(os.path.dirname(__file__), "templates")
@@ -589,6 +589,7 @@ def np_encoder(object, **kwargs):
589589
js_text=js_text,
590590
css_text=css_text,
591591
include_searchbar=include_searchbar,
592+
include_min_intersection_selector=include_min_intersection_selector
592593
)
593594

594595
return html

test/test_visuals.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,26 @@ def test_visualize_search_bar(self):
393393
include_searchbar=True,
394394
)
395395

396+
def test_visualize_min_intersection_selector(self):
397+
""" convenience test for generating a vis with a min_intersection_selector
398+
(and also with multiple color_values _and_ multiple node_color_values)"""
399+
mapper = KeplerMapper()
400+
data, labels = make_circles(1000, random_state=0)
401+
lens = mapper.fit_transform(data, projection=[0])
402+
graph = mapper.map(lens, data)
403+
color_values = lens[:, 0]
404+
405+
cv1 = np.array(lens)
406+
cv2 = np.flip(cv1)
407+
cv = np.column_stack([cv1, cv2])
408+
mapper.visualize(
409+
graph,
410+
color_values=cv,
411+
node_color_function=["mean", "std"],
412+
color_function_name=["hotdog", "hotdiggitydog"],
413+
include_min_intersection_selector=True,
414+
)
415+
396416
def test_visualize_graph_with_cluster_stats_above_below(self):
397417
mapper = KeplerMapper()
398418
X = np.ones((1000, 3))

0 commit comments

Comments
 (0)