Comparison slider for plots

Hi,
I was looking for a way to get a comparison slider like this: https://github.com/CodyHouse/image-comparison-slider
into a hv. Heatmap in my panel app. The bokeh people referred me to bokeh extensions. But I wonder now if directly in panel or holoviews would be the right place. Can someone help me finding the right place for a feature like this?

Hi @nritsche

As I see this there are two approaches.

The first might be the easiest to implement because you don’t have to learn how to write Bokeh extensions and you will mainly use the documentation of the image-comparison-slider.

For me solution two would probably take days to implement because there are many things to learn. But solution two is easier to use (don’t have to write custom template for every app) and powerful (can change things dynamically).

Could you post a screen shot of what you want to achieve? I’m not exactly sure from the description.

There’s a link to a demo in the repository I posted above. What I envision is just that. In a heat map. I would use that to apply filters to my data.

I’m sure it’s possible to write it as a bokeh extension, but I agree it’s a lot of work. How would you do that in a template?

@Marc I also tried explaining the idea in the bokeh discourse: https://discourse.bokeh.org/t/image-comparison-slider-extension-for-plot-overlays/6844

Hi @nritsche

A starting point for a custom template would be something like the below.

You would have to understand how the css and js script works and change it such that it works for two heatmaps instead of images.

"""Source: https://www.w3schools.com/howto/tryit.asp?filename=tryhow_js_image_compare"""
import panel as pn
import holoviews as hv
import numpy as np
import holoviews as hv
from holoviews import opts
hv.extension('bokeh')

template = """
{% extends base %}

{% block postamble %}
<style>
* {box-sizing: border-box;}

.img-comp-container {
  position: relative;
  height: 200px; /*should be the same height as the images*/
}

.img-comp-img {
  position: absolute;
  width: auto;
  height: auto;
  overflow:hidden;
}

.img-comp-img img {
  display:block;
  vertical-align:middle;
}

.img-comp-slider {
  position: absolute;
  z-index:9;
  cursor: ew-resize;
  /*set the appearance of the slider:*/
  width: 40px;
  height: 40px;
  background-color: #2196F3;
  opacity: 0.7;
  border-radius: 50%;
}
</style>
<script>
function initComparisons() {
  var x, i;
  /*find all elements with an "overlay" class:*/
  x = document.getElementsByClassName("img-comp-overlay");
  for (i = 0; i < x.length; i++) {
    /*once for each "overlay" element:
    pass the "overlay" element as a parameter when executing the compareImages function:*/
    compareImages(x[i]);
  }
  function compareImages(img) {
    var slider, img, clicked = 0, w, h;
    /*get the width and height of the img element*/
    w = img.offsetWidth;
    h = img.offsetHeight;
    /*set the width of the img element to 50%:*/
    img.style.width = (w / 2) + "px";
    /*create slider:*/
    slider = document.createElement("DIV");
    slider.setAttribute("class", "img-comp-slider");
    /*insert slider*/
    img.parentElement.insertBefore(slider, img);
    /*position the slider in the middle:*/
    slider.style.top = (h / 2) - (slider.offsetHeight / 2) + "px";
    slider.style.left = (w / 2) - (slider.offsetWidth / 2) + "px";
    /*execute a function when the mouse button is pressed:*/
    slider.addEventListener("mousedown", slideReady);
    /*and another function when the mouse button is released:*/
    window.addEventListener("mouseup", slideFinish);
    /*or touched (for touch screens:*/
    slider.addEventListener("touchstart", slideReady);
    /*and released (for touch screens:*/
    window.addEventListener("touchend", slideFinish);
    function slideReady(e) {
      /*prevent any other actions that may occur when moving over the image:*/
      e.preventDefault();
      /*the slider is now clicked and ready to move:*/
      clicked = 1;
      /*execute a function when the slider is moved:*/
      window.addEventListener("mousemove", slideMove);
      window.addEventListener("touchmove", slideMove);
    }
    function slideFinish() {
      /*the slider is no longer clicked:*/
      clicked = 0;
    }
    function slideMove(e) {
      var pos;
      /*if the slider is no longer clicked, exit this function:*/
      if (clicked == 0) return false;
      /*get the cursor's x position:*/
      pos = getCursorPos(e)
      /*prevent the slider from being positioned outside the image:*/
      if (pos < 0) pos = 0;
      if (pos > w) pos = w;
      /*execute a function that will resize the overlay image according to the cursor:*/
      slide(pos);
    }
    function getCursorPos(e) {
      var a, x = 0;
      e = e || window.event;
      /*get the x positions of the image:*/
      a = img.getBoundingClientRect();
      /*calculate the cursor's x coordinate, relative to the image:*/
      x = e.pageX - a.left;
      /*consider any page scrolling:*/
      x = x - window.pageXOffset;
      return x;
    }
    function slide(x) {
      /*resize the image:*/
      img.style.width = x + "px";
      /*position the slider:*/
      slider.style.left = img.offsetWidth - (slider.offsetWidth / 2) + "px";
    }
  }
}
</script>
{% endblock %}

<!-- goes in body -->
{% block contents %}
<h1>{{app_title}}</h1>

<div class="img-comp-container">
  <div class="img-comp-img">
    {{ embed(roots.A) }}
  </div>
  <div class="img-comp-img img-comp-overlay">
    <img src="https://www.w3schools.com/howto/img_forest.jpg" width="300" height="200">
  </div>
</div>

<script>
/*Execute a function that will execute an image compare function for each element with the img-comp-overlay class:*/
initComparisons();
</script>
{{ embed(roots.B) }}
{% endblock %}
"""

tmpl = pn.Template(template)

tmpl.add_variable('app_title', '<h1>Plot Comparison Slider App</h1>')
data = [(i, chr(97+j),  i*j) for i in range(5) for j in range(5) if i!=j]
hm = hv.HeatMap(data).sort().opts(xticks=None)
tmpl.add_panel('A', hm)
tmpl.add_panel('B', hv.Curve([1, 2, 3]))

tmpl.servable()

Please share your questions and findings as you go. Then we can all learn. Thanks.