From d49d9b03a09a9980668524e2f09acf588ead711f Mon Sep 17 00:00:00 2001
From: Marcus Poeckelmann <marcus.poeckelmann@informatik.uni-halle.de>
Date: Mon, 5 Dec 2022 16:44:40 +0100
Subject: [PATCH] v2.2: added brush_end_callback and bugfixes for brush

---
 LICENSE               |   2 +-
 README                |   2 +-
 catview.js            | 180 ++++++++++++++++++++++++++++--------------
 demo/demo_bottom.html |   5 ++
 demo/demo_left.html   |   5 ++
 demo/demo_right.html  |   5 ++
 demo/demo_top.html    |   5 ++
 7 files changed, 144 insertions(+), 60 deletions(-)
 mode change 100644 => 100755 catview.js

diff --git a/LICENSE b/LICENSE
index d709441..bef0602 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2015-2021 Marcus Pöckelmann
+Copyright (c) 2015 ongoing Marcus Pöckelmann
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README b/README
index c62d4fa..c58f20c 100644
--- a/README
+++ b/README
@@ -1,6 +1,6 @@
 Thank you for using CATview - the Colored & Aligned Texts view.
 
-This is version 2.0.
+This is version 2.2.
 
 For information about the features and usage of CATview, please visit:
 https://catview.uzi.uni-halle.de
diff --git a/catview.js b/catview.js
old mode 100644
new mode 100755
index 352c717..c1a143e
--- a/catview.js
+++ b/catview.js
@@ -1,7 +1,7 @@
 /*
  The MIT License (MIT)
 
- Copyright (c) 2015-2021 Marcus Pöckelmann
+ Copyright (c) 2015 ongoing Marcus Pöckelmann
 
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
@@ -23,7 +23,7 @@
 */
 
 // CATview - the Colored & Aligned Texts view
-// version 2.1
+// version 2.2
 const CATview = new function() {
   this.debug = false;
 
@@ -32,7 +32,7 @@ const CATview = new function() {
     if(CATview.debug)
       console.log('CATview.initialize');
 
-    CATview.version = '2.1';
+    CATview.version = '2.2';
 
     // id of the parent container that will include CATview
     CATview.parent_id = 'CATview';
@@ -81,9 +81,13 @@ const CATview = new function() {
     CATview.display_brush = false;
     CATview.brush_from_edge = -1;
     CATview.brush_to_edge = -1;
+    CATview.brush_range = 1;
     CATview.brush_from_name = -1;
     CATview.brush_to_name = -1;
     CATview.brush_callback = null;
+    CATview.brush_end_callback = null;
+    CATview.brush_offset_from = -0.5;
+    CATview.brush_offset_to = 0.5;
 
     // zooming parameters
     CATview.zoom = null;    // the d3 object for zooming
@@ -374,12 +378,13 @@ const CATview = new function() {
   // define the interval of edges and refresh the content afterwards
   this.refresh_content = function(from, to){
     if(CATview.debug)
-      console.log('CATview.refresh_content');
+      console.log('CATview.refresh_content(' + from + ', ' + to + ')');
 
     if(CATview.content){
       if (from != null) CATview.from = from;
       if (from == 0) CATview.from = 1;
       if (to != null)   CATview.to = to;
+      if (to > CATview.edges.length) CATview.to = CATview.edges.length;
       // refresh the edges-scale (with one bin offset)
       CATview.set_scale_x(Math.max(CATview.from - 1, 0), Math.min(CATview.to + 1, CATview.edges.length + 1))
 
@@ -425,7 +430,7 @@ const CATview = new function() {
   };
   this.draw_names_axis = function(){
     if(CATview.debug)
-      console.log('CATview.draw_names_axis')
+      console.log('CATview.draw_names_axis');
 
     let names_axis = CATview.svg.select(".names-axis");
     names_axis.selectAll('*').remove(); // remove the current content
@@ -472,7 +477,7 @@ const CATview = new function() {
   // draws the zebra stripes (gray lines in the background for every second text)
   this.draw_zebra_stripes = function(){
     if(CATview.debug)
-      console.log('CATview.draw_zebra_lines')
+      console.log('CATview.draw_zebra_lines');
 
     let zebra = CATview.svg.select(".zebra");
     zebra.selectAll('*').remove();  // remove the current content
@@ -503,7 +508,7 @@ const CATview = new function() {
   // draws the icons for tools included in CATview.tools
   this.draw_tool_icons = function(){
     if(CATview.debug)
-      console.log('CATview.draw_tool_icons')
+      console.log('CATview.draw_tool_icons');
 
     let toolicons = CATview.svg.select(".tool-icons");
     toolicons.selectAll('*').remove();  // remove the current content
@@ -1053,18 +1058,16 @@ const CATview = new function() {
 
       // refresh the brush
       if(CATview.brush) {
-        // remember the current extent
-        //let extent = CATview.brush.extent();
-        // update the scale
-       // if(CATview.vertical == true)
-       // todo   CATview.brush.y(CATview.scale_edges);
-       // else
-        // todo   CATview.brush.x(CATview.scale_edges);
-        // restore the extent
-        //CATview.brush.extent(extent);
-        // redraw the brush
-        //CATview.content.select(".brush").call(CATview.brush);
-      }
+        let from = CATview.brush_from_edge + 1.0 + CATview.brush_offset_from;
+        let to = CATview.brush_to_edge + 1.0 + CATview.brush_offset_to;
+        if(CATview.x_inverted){
+          let swap = from;
+          from = to;
+          to = swap;
+        }
+        CATview.brush.move(d3.select('.brush'), [from, to].map(CATview.scale_edges))
+        // TODO prevent brushing listeners
+      }      
     }
     else
       return false;
@@ -1077,8 +1080,9 @@ const CATview = new function() {
     return true;
   };
 
-  // enable the brush functionality and pass a callback function to the brush (will receive: from_edge, to_edge, from_name, to_name)
-  this.enable_brush = function(brush_callback){
+  // enable the brush functionality and pass
+  // a callback function to the brush (will receive: from_edge, to_edge, from_name, to_name)
+  this.enable_brush = function(_brush_callback, _brush_end_callback){
     if(CATview.debug)
       console.log('CATview.enable_brush');
 
@@ -1088,8 +1092,10 @@ const CATview = new function() {
       if(CATview.content)
         CATview.draw_brush();
     }
-    if(brush_callback != null)
-      CATview.brush_callback = brush_callback;
+    if(_brush_callback != null)
+      CATview.brush_callback = _brush_callback;
+    if(_brush_end_callback != null)
+      CATview.brush_end_callback = _brush_end_callback;
 
     return true;
   };
@@ -1106,7 +1112,6 @@ const CATview = new function() {
       CATview.brush_to_edge = -1;
       CATview.brush_from_name = -1;
       CATview.brush_to_name = -1;
-      // CATview.brush_callback = null;
       // remove the brush from CATview
       if (CATview.content)
         CATview.content.select(".brush").remove();
@@ -1130,23 +1135,25 @@ const CATview = new function() {
 
         if(CATview.vertical == true) {
           CATview.brush = d3.brushY()
-          .on("brush", CATview.brushing);
+          .on("brush", CATview.brushing)
+          .on("end", CATview.brush_end);
 
           if(CATview.x_inverted)
-            CATview.brush.extent([[CATview.scale_names.range()[0], CATview.scale_edges.range()[1]], [CATview.scale_names.range()[1], CATview.scale_edges.range()[0]]])
+            CATview.brush.extent([[CATview.scale_names.range()[0], CATview.scale_edges.range()[1]], [CATview.scale_names.range()[1], CATview.scale_edges.range()[0]]]);
           else
-            CATview.brush.extent([[CATview.scale_names.range()[0], CATview.scale_edges.range()[0]], [CATview.scale_names.range()[1], CATview.scale_edges.range()[1]]])
+            CATview.brush.extent([[CATview.scale_names.range()[0], CATview.scale_edges.range()[0]], [CATview.scale_names.range()[1], CATview.scale_edges.range()[1]]]);
         } else {
           CATview.brush = d3.brushX()
-            .on("brush", CATview.brushing);
+            .on("brush", CATview.brushing)
+            .on("end", CATview.brush_end);
 
           if(CATview.x_inverted)
-            CATview.brush.extent([[CATview.scale_edges.range()[1], CATview.scale_names.range()[0]], [CATview.scale_edges.range()[0], CATview.scale_names.range()[1]]])
+            CATview.brush.extent([[CATview.scale_edges.range()[1], CATview.scale_names.range()[0]], [CATview.scale_edges.range()[0], CATview.scale_names.range()[1]]]);
           else
-            CATview.brush.extent([[CATview.scale_edges.range()[0], CATview.scale_names.range()[0]], [CATview.scale_edges.range()[1], CATview.scale_names.range()[1]]])
+            CATview.brush.extent([[CATview.scale_edges.range()[0], CATview.scale_names.range()[0]], [CATview.scale_edges.range()[1], CATview.scale_names.range()[1]]]);
         }
 
-        let brush = CATview.content.append("g")
+        CATview.content.append("g")
         .attr("class", "brush")
         .call(CATview.brush);
       }
@@ -1162,51 +1169,108 @@ const CATview = new function() {
 
     return true;
   };
+  this.set_brush_end_callback = function(_brush_end_callback) {
+    CATview.brush_end_callback = _brush_end_callback;
+
+    return true;
+  };
 
   // react on changes of the brush
   this.brushing = function() {
     if(CATview.debug)
       console.log('CATview.brushing');
-
-    // var edges_index = CATview.vertical == true ? 1 : 0;
-    // var names_index = CATview.vertical == true ? 0 : 1;
-
-    // get the interval as integers
-    // var from_edge = Math.ceil(CATview.brush.extent()[0][edges_index]) - 1;
-    let from_edge = Math.ceil(d3.event.selection.map(CATview.scale_edges.invert)[0]) - 1;
-    if(from_edge < 0)
-      from_edge = 0;
-    // var to_edge = Math.floor(CATview.brush.extent()[1][edges_index]) - 1;
-    let to_edge = Math.floor(d3.event.selection.map(CATview.scale_edges.invert)[1]) - 1;
-    if(to_edge >= CATview.edges.length)
-      to_edge = CATview.edges.length - 1;
-    let from_name = null; // Math.ceil(CATview.brush.extent()[0][names_index]);
-    let to_name = null; // Math.floor(CATview.brush.extent()[1][names_index]);
-
-    if(CATview.x_inverted){
-      let swap = from_edge;
-      from_edge = to_edge;
-      to_edge = swap;
-    }
+  
+    // handle inverted edges axis
+    let from = CATview.x_inverted ? 1 : 0;
+
+    // the starting position is construction by:
+    //    d3.event.selection[0] → current begin of the drawn selection window
+    //    CATview.brush_offset_from → the specified offset used to place the window at the begin of a rectangle
+    //    0.001 → a little additional offset for the case that the window was dragged to the most right position
+    let from_edge = Math.round(CATview.scale_edges.invert(d3.event.selection[from]) - CATview.brush_offset_from - 0.001) -1;
+    if(from_edge < CATview.from - 1) from_edge = CATview.from - 1;
+    else if(from_edge >= CATview.to) from_edge = CATview.to - 1;
+    
+    // add the current range to the from position to get the to position
+    let to_edge = from_edge + CATview.brush_range - 1;
+
+    // TODO may allow selection of names axis
+    //let from_name = null; // Math.ceil(CATview.brush.extent()[0][names_index]);
+    //let to_name = null; // Math.floor(CATview.brush.extent()[1][names_index]);
 
     if(to_edge - from_edge >= 0) {
       // check whether the interval has changed
-      if (from_edge != CATview.brush_from_edge || to_edge != CATview.brush_to_edge || from_name != CATview.brush_from_name || to_name != CATview.brush_to_name) {
+      if (from_edge != CATview.brush_from_edge || to_edge != CATview.brush_to_edge){ // || from_name != CATview.brush_from_name || to_name != CATview.brush_to_name) {
         // save the current interval
         CATview.brush_from_edge = from_edge;
         CATview.brush_to_edge = to_edge;
-        CATview.brush_from_name = from_name;
-        CATview.brush_to_name = to_name;
+        //CATview.brush_from_name = from_name;
+        //CATview.brush_to_name = to_name;
         // call the brush_callback as the interval has changed
         if (CATview.brush_callback != null) {
-          CATview.brush_callback(from_edge, to_edge, from_name, to_name);
+          CATview.brush_callback(from_edge, to_edge, null, null); //, from_name, to_name);
         }
         else {
-          console.log('CATview brush edge from ' + from_edge + ' to ' + to_edge + ' and name from ' + from_name + ' to ' + to_name);
+          // no callback specified → log positions to console
+          console.log('CATview brush edge from ' + from_edge + ' to ' + to_edge);// + ' and name from ' + from_name + ' to ' + to_name);
         }
       }
     }
   };
+  // react on the end of a brush gesture
+  this.brush_end = function() {
+    if(CATview.debug)
+      console.log('CATview.brush_end');
+
+    if (!d3.event.sourceEvent) return; // only transition after input
+    if (!d3.event.selection) return; // ignore empty selections
+
+    // handle inverted edges axis
+    let from = CATview.x_inverted ? 1 : 0;
+    let to = CATview.x_inverted ? 0 : 1;
+
+    // the starting position (end analogously) is construction by:
+    //    d3.event.selection[0] → current begin of the drawn selection window
+    //    CATview.brush_offset_from → the specified offset used to place the window at the begin of a rectangle
+    //    0.001 → a little additional offset for the case that the window was dragged to the most right position
+    let from_edge = Math.round(CATview.scale_edges.invert(d3.event.selection[from]) - CATview.brush_offset_from - 0.001) -1;
+    if(from_edge < CATview.from - 1) from_edge = CATview.from - 1;
+    else if(from_edge >= CATview.to) from_edge = CATview.to - 1;
+    let to_edge = Math.round(CATview.scale_edges.invert(d3.event.selection[to]) - CATview.brush_offset_to + 0.001) -1;
+    if(to_edge >= CATview.to) to_edge = CATview.to - 1;
+    else if(to_edge < CATview.from - 1) to_edge = CATview.from - 1;
+
+    // calculated the number of rectangles within the window
+    let brush_range = to_edge - from_edge + 1;
+ 
+    // intervene if range is zero
+    if(brush_range == 0){
+      console.log('intervene')
+      to_edge = from_edge;
+      brush_range = 1;
+    }
+
+    // smoothly redraw the brush to discrete positions (begin and end of the rectangles)
+    if(CATview.x_inverted){
+      d3.select(this).transition().call(d3.event.target.move,
+          [to_edge + 1.0 + CATview.brush_offset_to, from_edge + 1.0 + CATview.brush_offset_from].map(CATview.scale_edges));
+    }else{
+      d3.select(this).transition().call(d3.event.target.move,
+          [from_edge + 1.0 + CATview.brush_offset_from, to_edge + 1.0 + CATview.brush_offset_to].map(CATview.scale_edges));
+    }
+
+    // save new values if there was a change
+    if(from_edge !== CATview.brush_from_edge || to_edge !== CATview.brush_to_edge || brush_range !== CATview.brush_range){
+      // save the current interval
+      CATview.brush_from_edge = from_edge;
+      CATview.brush_to_edge = to_edge;
+      CATview.brush_range = brush_range;
+      // and call the custom callbacks
+      if (CATview.brush_end_callback != null) {
+        CATview.brush_end_callback(from_edge, to_edge); //, from_name, to_name);
+      }
+    }
+  };
 
   // add a new tool icon and its behavior
   this.add_tool = function(_icon, _callback, _index, _rotation){
@@ -1398,4 +1462,4 @@ const CATview = new function() {
   // - may: method for changing the border_color → set_border(_color, _width)
   // - adopt font size in respect to the number of witnesses
   // - method to reset all data → reset
-};
\ No newline at end of file
+};
diff --git a/demo/demo_bottom.html b/demo/demo_bottom.html
index 1c1e418..849851c 100644
--- a/demo/demo_bottom.html
+++ b/demo/demo_bottom.html
@@ -157,5 +157,10 @@
       // refresh CATviews scrollspy
       CATview.draw_scroll_spy(from);
     });
+    
+    // reacts as soon as the size of the selection box (brush) is fixed
+    CATview.set_brush_end_callback(function(from, to){
+      CATview.brush_callback(from, to);
+    });
   </script>
 </html>
diff --git a/demo/demo_left.html b/demo/demo_left.html
index ba1e875..8d19397 100644
--- a/demo/demo_left.html
+++ b/demo/demo_left.html
@@ -155,5 +155,10 @@
       // refresh CATviews scrollspy
       CATview.draw_scroll_spy(from);
     });
+    
+    // reacts as soon as the size of the selection box (brush) is fixed
+    CATview.set_brush_end_callback(function(from, to){
+      CATview.brush_callback(from, to);
+    });
  </script>
 </html>
diff --git a/demo/demo_right.html b/demo/demo_right.html
index 5502515..8bcb7ab 100644
--- a/demo/demo_right.html
+++ b/demo/demo_right.html
@@ -155,5 +155,10 @@
       // refresh CATviews scrollspy
       CATview.draw_scroll_spy(from);
     });
+    
+    // reacts as soon as the size of the selection box (brush) is fixed
+    CATview.set_brush_end_callback(function(from, to){
+      CATview.brush_callback(from, to);
+    });
   </script>
 </html>
diff --git a/demo/demo_top.html b/demo/demo_top.html
index 2252784..0548081 100644
--- a/demo/demo_top.html
+++ b/demo/demo_top.html
@@ -158,5 +158,10 @@
       // refresh CATviews scrollspy
       CATview.draw_scroll_spy(from);
     });
+    
+    // reacts as soon as the size of the selection box (brush) is fixed
+    CATview.set_brush_end_callback(function(from, to){
+      CATview.brush_callback(from, to);
+    });
   </script>
 </html>
-- 
GitLab