Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add ability to d3 vis to live-update min-intersection requirement #231

Merged
merged 3 commits into from
Oct 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Release log


## Unreleased

### Added
- ability to live-update the min-intersection threshold for edges on the d3 vis (#231)


## 2.0.1

### Fixed
Expand Down
8 changes: 7 additions & 1 deletion kmapper/kmapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
92 changes: 85 additions & 7 deletions kmapper/static/kmapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -282,7 +276,7 @@ function start() {
simulation.force('link').links(links);
simulation.alpha(1).restart()

reset_color_functions()
update_color_functions()
}

init();
Expand Down Expand Up @@ -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');

Expand All @@ -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.
}
Expand Down
11 changes: 11 additions & 0 deletions kmapper/templates/toolbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,15 @@ <h3>Node Color Function
</div>
{% endif %}

{% if include_min_intersection_selector %}
<div id='min_intersction_selector' class="tool_item">
<form>
<h3>Min Intersection Selector
<input type="number" min="1" value="{{ mapper_summary.custom_meta.nerve_min_intersection }}">
<button class='btn' type='submit'>Update</button>
</h3>
</form>
</div>
{% endif %}

</div>
3 changes: 2 additions & 1 deletion kmapper/visuals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions test/test_visuals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down