1

I'd like to render a time x-axis on a HTML canvas, that adapts when I pan (with click+drag) or when I zoom.

Charting JS libraries might exist to do this for curves/charts, but in my case, the data over the x-axis will not be a curve, so I need to do it from scratch.

Here when I click+drag on the background, the origin x adapts, that is good (see snippet). But how to render a text x-axis like this on the bottom of the canvas?

--------------------------------------------------
|           |           |            |           |
jan 02      jan 03      jan 04       jan 05      jan 06

var drag = false;
var x = 0;
var last_position = {};

document.getElementById('canvas').onmousedown = function() { drag = true; }
document.getElementById('canvas').onmouseup = function() { drag = false; }
document.getElementById('canvas').onmousemove = function(e) { 
  var deltaX = last_position.x - e.clientX,
      deltaY = last_position.y - e.clientY;
  if (drag && typeof(last_position.x) != 'undefined') { 
    x += deltaX;
    document.getElementById('pos').innerHTML = x; 
  } 
  last_position = { x : e.clientX, y : e.clientY };  
}
#canvas { width: 400px; height: 150px; background-color: #ccc; }
<canvas id="canvas"></canvas>
<div id="pos"></div>
Basj
  • 41,386
  • 99
  • 383
  • 673

2 Answers2

2

All you need to do is subtract the x position from the desired screen coordinate, see the example below.

/* 
 * Your original code: 
 */
var drag = false;
var x = 0;
var last_position = {};

document.getElementById('canvas').onmousedown = function() { drag = true; }
document.getElementById('canvas').onmouseup = function() { drag = false; }
document.getElementById('canvas').onmousemove = function(e) { 
  var deltaX = last_position.x - e.clientX,
      deltaY = last_position.y - e.clientY;
  if (drag && typeof(last_position.x) != 'undefined') { 
    x += deltaX;
    document.getElementById('pos').innerHTML = x; 
  } 
  last_position = { x : e.clientX, y : e.clientY };
}

/*
 * A simple draw text method:
 */
function drawText(context, text, x, y) {
  context.font = "12px Arial";
  context.fillStyle = "red";
  context.textAlign = "center";
  context.fillText(text, x, y);
}

/*
 * The below uses 'requestAnimationFrame' to run a rendering loop:
 * For more information see https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame 
 */
function render() {

  var canvas = document.getElementById('canvas')
  var context = canvas.getContext("2d");

  /* We must clear the canvas before rendering anything else... */
  context.clearRect(0, 0, canvas.width, canvas.height);

  /* This could be improved a fair bit, consider using a loop / non magic numbers. */
  drawText(context, "Jan 1st", 30 - x, canvas.height - 12);
  drawText(context, "Jan 2nd", 100 - x, canvas.height - 12);
  drawText(context, "Jan 3rd", 170 - x, canvas.height - 12);
  drawText(context, "Jan 4th", 240 - x, canvas.height - 12);
  drawText(context, "Jan 5th", 310 - x, canvas.height - 12);

  requestAnimationFrame(render);
}
render();
#canvas { width: 400px; height: 150px; background-color: #ccc; }
<canvas id="canvas"></canvas>
<div id="pos">0</div>

Note that this could be improved a fair bit in terms of efficiency, and cleanliness - I.e you shouldn't render text which is offscreen.

  • Thanks a lot! It's the idea! In fact, I would like it to be an infinite x-axis, i.e. new dates are added on the fly (I should have added this in the question), how would you do this? – Basj Jan 17 '18 at 19:29
  • That's a much different question that the one purposed in the main post. I would recommend you create a new post regarding that. –  Jan 17 '18 at 20:15
  • 1
    I just posted an answer (inspired by yours) @JacobPersi: https://stackoverflow.com/a/48309378/1422096. – Basj Jan 17 '18 at 20:22
1

Based on JacobPersi's answer (99,9% credit to him), here is a solution:

function formatDate(date) {
  var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  var day = date.getDate(), monthIndex = date.getMonth(), year = date.getFullYear().toString().substr(-2);
  return day + ' ' + monthNames[monthIndex] + ' ' + year;
}

var drag = false;
var x = 0;
var last_position = {};

var canvas = document.getElementById('canvas');
canvas.width = canvas.getBoundingClientRect().width;  // important to prevent blurry text issues: https://stackoverflow.com/q/15661339/1422096
canvas.height = canvas.getBoundingClientRect().height;
canvas.onmousedown = function() { drag = true; }
document.onmouseup = function() { drag = false; }
canvas.onmousemove = function(e) { 
  var deltaX = last_position.x - e.clientX,
      deltaY = last_position.y - e.clientY;
  if (drag && typeof(last_position.x) != 'undefined') { 
    x += deltaX;
  } 
  last_position = { x : e.clientX, y : e.clientY };
}

function drawText(context, text, x, y) {
  context.font = "10px Arial";
  context.fillStyle = "black";
  context.textAlign = "center";
  context.fillText(text, x, y);
}

function render() {

  var canvas = document.getElementById('canvas')
  var context = canvas.getContext("2d");

  context.clearRect(0, 0, canvas.width, canvas.height);

  for (var i = Math.round((-2000+x)/200); 200 * i - x < 2000; i++) {
      var d = new Date();
      d.setTime(d.getTime() + i * 3600 * 24 * 1000);
      var s = formatDate(d);       
      drawText(context, s, 200 * i - x, canvas.height - 8);
  }
  requestAnimationFrame(render);
}
render();
* { margin: 0; padding: 0; border: 0; }
body, html { height: 100% }
#canvas { width: 100%; height: 75%; background-color: #ccc; }
#canvas2 { width: 100%; height: 25%; background-color: #aaa; }
<canvas id="canvas"></canvas>
<canvas id="canvas2"></canvas>
Basj
  • 41,386
  • 99
  • 383
  • 673