sean le roy
sean le roy

Reputation: 567

HTML Canvas Zoom to region on graph

just wondering has anyone got any solution for zooming into a region on a graph. When I say region I mean, you mouse down at a point on the canvas, move the mouse to another location while still pressed, and when mouse up I want to zoom into that block. and only scale the x axis, no the the y.

This is exactly what I want to do - http://canvasjs.com/docs/charts/basics-of-creating-html5-chart/zooming-panning/

Now I've been able to zoom in, but my graph is a mess after the zoom. Lines and points are all stretched, twisted etc, which I've tried to adjust using the scale.

This is what I've been doing so far.

let y = heightOfCanvas / 2,

    // Get the region in pixels to zoom to. If canvas width
    // is 500, this is lets say 100px to 400px. 
    regionToZoom = toX - fromX,

    // Subtract from actual width, so we get what will be scaled
    // out.
    difference = widthOfCanvas - regionToZoom,

    // Scale = fullWidth / partWidth
    scale = widthOfCanvas / difference,

    // This is how I worked out the value to translate back,
    // so that it lines up right
    translateBack = widthOfCanvas * scale



    ctx.translate(from, y);

    ctx.scale(scale, 1);

    from = from * translateBack;

    ctx.translate(-from, -y);

    // REDRAW GRAPH AFTER

Like this zooms in fine, but as I said, it becomes a mess after.

If anyone has done something similar in the past, like in the example above, I would be so grateful for the help.

Sean.

Upvotes: 0

Views: 2159

Answers (1)

markE
markE

Reputation: 105015

Here's a quick example showing how to select & display a subset of a graph

// canvas vars
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.font='14px arial';
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
    var BB=canvas.getBoundingClientRect();
    offsetX=BB.left;
    offsetY=BB.top;        
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }

var isDrilled=false;
var isDown=false;
var startX,startY;

// graph vars
var axisY=ch/3;
var points=[];

// DEMO: add random data points
var demoPointCount=50;
for(var i=0;i<demoPointCount;i++){
    points.push({y:Math.random()*150-75});
}

// draw the full data graph
draw(points,axisY);

// listen to mouse events
window.onmousedown=(function(e){handleMouseDown(e);});
window.onmousemove=(function(e){handleMouseMove(e);});
window.onmouseup=(function(e){handleMouseUpOut(e);});
window.onmouseout=(function(e){handleMouseUpOut(e);});


function draw(pts,axisY,startX,mouseX,drillStart,drillEnd){
    // redraw the given pts data
    ctx.clearRect(0,0,cw,ch);
    ctx.beginPath();
    ctx.moveTo(0,pts[0].y+axisY);
    for(var i=0;i<pts.length;i++){
        var x=cw/(pts.length-1)*i;
        ctx.lineTo(x,pts[i].y+axisY);
    }
    ctx.stroke();
    // used when highlighting a drilldown section of full data
    if(startX && mouseX){
        ctx.globalAlpha=0.10;
        ctx.fillStyle='black';
        ctx.fillRect(startX,0,mouseX-startX,ch);
        ctx.globalAlpha=1.00;
    }else if(drillStart && drillEnd){
        ctx.fillText('Viewing '+drillStart+' - '+drillEnd+'. Click to return to all data view.',10,20);
    }else{
        ctx.fillText('Drag to select data to drill into.',10,20);
    }
}

function handleMouseDown(e){
  // if displaying drilled data, return to full data
  if(isDrilled){
      isDrilled=false;
      draw(points,axisY);
      return;
  }
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  // start mouse position
  startX=parseInt(e.clientX-offsetX);
  startY=parseInt(e.clientY-offsetY);
  // Put your mousedown stuff here
  isDown=true;
}

function handleMouseUpOut(e){
  if(!isDown){return;}
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  // mouse position
  mouseX=parseInt(e.clientX-offsetX);
  mouseY=parseInt(e.clientY-offsetY);
  // Put your mouseup stuff here
  isDown=false;
  isDrilled=true;
  // normalize
  if(mouseX<startX){ var t=startX; startX=mouseX; mouseX=t; }
  // fetch highlighted start & end data points
  drillStart=parseInt(startX/cw*points.length);
  drillEnd=parseInt(mouseX/cw*points.length);
  var subset=points.slice(drillStart,drillEnd+1);
  // draw the data subset
  draw(subset,axisY,null,null,drillStart,drillEnd);
}

function handleMouseMove(e){
  if(!isDown){return;}
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  // mouse position
  mouseX=parseInt(e.clientX-offsetX);
  mouseY=parseInt(e.clientY-offsetY);
  // Put your mousemove stuff here
  draw(points,axisY,startX,mouseX);
}
body{ background-color:white; }
#canvas{border:1px solid red; }
<canvas id="canvas" width=512 height=512></canvas>

[Previous outdated answer]

Here's a way to zoom without all the fuss about remembering transformations

This is a quick-thought so some tinkering may be needed.

  1. Copy the graph by drawing it on a second in-memory canvas:

    var memCanvas=myCanvas.cloneNode();
    var memCtx=memCanvas.getContext('2d');
    memCtx.drawImage(myCanvas,0,0);
    
  2. DrawImage the memCanvas onto the displayed canvas:

    context.drawImage(memCanvas,0,0);
    
  3. On mousedown, grab the top-left coordinates of your selection box.

  4. On mouseup, grab the bottom-right coordinates of your selection box. Note: you might have to flip top & bottom or left & right if top>bottom or left>right.

  5. Calculate the scaling factor required to draw just the selection box full-frame into the visible canvas:

    var scale = Math.min(
        (myCanvas.width/selectionboxWidth),   
        (myCanvas.height/selectionboxHeight)
    );
    
  6. Use the clipping form of drawImage to pull just the selection box from the memCanvas and draw it scaled into the visible canvas:

    var x=sboxLeft;
    var y=sboxTop;
    var w=sboxRight-sboxLeft; 
    var h=sboxBottom-sboxTop;
    sboxcontext.drawImage(
        memCanvas,            // fetch from the memCanvas
        x,y,w,h,              // clip the selected box
        0,0,w*scale,h*scale); // scale the selected box into the visible canvas
    

Then when you want to unzoom you just redraw the full memCanvas to the visible canvas (no need to untransform). The resulting graph is not a mess!

context.drawImage(memCanvas,0,0);  // unzoomed -- simply!

Upvotes: 1

Related Questions