Add Link to X-Label Chart.js - javascript

I'm looking to be able to link the x-labels in a Chart.js bar chart. I've searched pretty thoroughly, and ended up trying to come up with my own solution: because the labels correspond to the bars directly above them and Chart.js has a built in getBarsAtEvent(evt) method, I tried creating an event if the user didn't click on a chart - this new event had pageX and pageY that was directly above the initial click such that if the user had clicked on a label, the new event would simulate a click on the bar graph.
However, calling getBarsAtEvent(createdClickEvent) repeatedly gives me a Uncaught TypeError ("Cannot read property 'getBoundingClientRect' of null"), which must mean that the getBarsAtEvent method, when called on my simulated click, isn't actually returning anything.
Any suggestions or alternate approaches would be greatly appreciated, thanks in advance.

An alternative approach would be to determine the point where the user is actually clicked and based on that calculate which label was clicked. For that you will need some information about the chart created and have to do some calculations.
Below is a way of doing that, and here is a Fiddle with this code/approach. Hope it helps.
$("#canvas").click(
function(evt){
var ctx = document.getElementById("canvas").getContext("2d");
// from the endPoint we get the end of the bars area
var base = myBar.scale.endPoint;
var height = myBar.chart.height;
var width = myBar.chart.width;
// only call if event is under the xAxis
if(evt.pageY > base){
// how many xLabels we have
var count = myBar.scale.valuesCount;
var padding_left = myBar.scale.xScalePaddingLeft;
var padding_right = myBar.scale.xScalePaddingRight;
// calculate width for each label
var xwidth = (width-padding_left-padding_right)/count;
// determine what label were clicked on AND PUT IT INTO bar_index
var bar_index = (evt.offsetX - padding_left) / xwidth;
// don't call for padding areas
if(bar_index > 0 & bar_index < count){
bar_index = parseInt(bar_index);
// either get label from barChartData
console.log("barChartData:" + barChartData.labels[bar_index]);
// or from current data
var ret = [];
for (var i = 0; i < myBar.datasets[0].bars.length; i++) {
ret.push(myBar.datasets[0].bars[i].label)
};
console.log("current data:" + ret[bar_index]);
// based on the label you can call any function
}
}
}
);

I modified iecs's answer to work with chartjs 2.7.1
var that = this;
this.chart = new Chart($("#chart"), {
type: 'bar',
data: {
labels: labels,
datasets: datasets
},
options: {
events: ["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"],
onClick: function(e, data) {
var ctx = $("#chart")[0].getContext("2d");
var base = that.chart.chartArea.bottom;
var height = that.chart.chart.height;
var width = that.chart.chart.scales['x-axis-0'].width;
var offset = $('#chart').offset().top - $(window).scrollTop();
if(e.pageY > base + offset){
var count = that.chart.scales['x-axis-0'].ticks.length;
var padding_left = that.chart.scales['x-axis-0'].paddingLeft;
var padding_right = that.chart.scales['x-axis-0'].paddingRight;
var xwidth = (width-padding_left-padding_right)/count;
// don't call for padding areas
var bar_index = (e.offsetX - padding_left - that.chart.scales['y-axis-0'].width) / xwidth;
if(bar_index > 0 & bar_index < count){
bar_index = Math.floor(bar_index);
console.log(bar_index);
}
}
}
}
});
The main differences are:
The newer versions of chartjs use an chart.scales array instead of chart.scale with a bunch of values
I had to subtract chart.scales['y-axis-0'].width from the x offset to get the correct bar_index
I changed parseInt to Math.floor, just personal preference
And if you want the cursor to change when you hover over them, add "hover" to the events array and this to the options:
onHover: function(e) {
var ctx = $("#chart")[0].getContext("2d");
var base = that.chart.chartArea.bottom;
var height = that.chart.chart.height;
var width = that.chart.chart.scales['x-axis-0'].width;
var yOffset = $('#chart').offset().top - $(window).scrollTop();
var xOffset = $('#chart').offset().left - $(window).scrollLeft();
var left = xOffset + that.chart.scales['x-axis-0'].paddingLeft + that.chart.scales['x-axis-0'].left;
var right = xOffset + that.chart.scales['x-axis-0'].paddingRight + that.chart.scales['x-axis-0'].left + width;
if(e.pageY > base + yOffset && e.pageX > left && e.pageX < right){
e.target.style.cursor = 'pointer';
}
else {
e.target.style.cursor = 'default';
}
}

Related

Vis.js won't zoom-in further than scale 1.0 with .fit()

I am using the library Vis.js to display a Network.
For my application, I need the network to be displayed fullscreen, with the nodes almost touching the borders of its container.
The problem comes from network.fit(); it won't Zoom-In further than scale '1.0'
I wrote a Fiddle to showcase the issue:
http://jsfiddle.net/v1467x1d/12/
var nodeSet = [
{id:1,label:'big'},
{id:2,label:'big too'} ];
var edgeSet = [
{from:1, to:2} ];
var nodes = new vis.DataSet(nodeSet);
var edges = new vis.DataSet(edgeSet);
var container = document.getElementById('mynetwork');
var data = {
nodes: nodes,
edges: edges
};
var options = {};
var network = new vis.Network(container, data, options);
network.fit();
console.log( 'scale: '+ network.getScale() ); // Always 1
How can I force Vis to zoom until the network is fullscreen?
As Richard said - now, this method does not work as expected. You can use a custom method, as a concept:
function bestFit() {
network.moveTo({scale:1});
network.stopSimulation();
var bigBB = { top: Infinity, left: Infinity, right: -Infinity, bottom: -Infinity }
nodes.getIds().forEach( function(i) {
var bb = network.getBoundingBox(i);
if (bb.top < bigBB.top) bigBB.top = bb.top;
if (bb.left < bigBB.left) bigBB.left = bb.left;
if (bb.right > bigBB.right) bigBB.right = bb.right;
if (bb.bottom > bigBB.bottom) bigBB.bottom = bb.bottom;
})
var canvasWidth = network.canvas.body.container.clientWidth;
var canvasHeight = network.canvas.body.container.clientHeight;
var scaleX = canvasWidth/(bigBB.right - bigBB.left);
var scaleY = canvasHeight/(bigBB.bottom - bigBB.top);
var scale = scaleX;
if (scale * (bigBB.bottom - bigBB.top) > canvasHeight ) scale = scaleY;
if (scale>1) scale = 0.9*scale;
network.moveTo({
scale: scale,
position: {
x: (bigBB.right + bigBB.left)/2,
y: (bigBB.bottom + bigBB.top)/2
}
})
}
[ http://jsfiddle.net/dv4qyeoL/ ]
I am sorry it is impossible to do this using network.fit. Here is the relevant code.
However, you can patch it yourself and include the modified version into your application which should then work as expected.
Here is a fiddle (line 38337 for the modification). I can't promise it won't break something else though.
Relevant code :
/*if (zoomLevel > 1.0) {
zoomLevel = 1.0;
} else if (zoomLevel === 0) {
zoomLevel = 1.0;
}*/
if (zoomLevel === 0) {
zoomLevel = 1.0;
}

Keeping a portion of an image always visible on the canvas

I want to make sure that my instance of fabric.Image is always visible.
It doesn't need to be 100% visible, but a portion of it should always be seen. I've seen a few other questions that are similar and I've based my solution on them, but I haven't found anything that completely solves the problem.
My current solution works when no rotation (angles) have been applied to the image. If the angle is at 0 then this solution is perfect, but once the angle start changing I'm having problems.
this.canvas.on('object:moving', function (e) {
var obj = e.target;
obj.setCoords();
var boundingRect = obj.getBoundingRect();
var max_pad_left_over_width = 50;//obj.currentWidth * .08;
var max_pad_left_over_height = 50;//obj.currentHeight * .08;
var max_top_pos = -(obj.currentHeight - max_pad_left_over_height);
var max_bottom_pos = obj.canvas.height - max_pad_left_over_height;
var max_left_pos = -(obj.currentWidth - max_pad_left_over_width);
var max_right_pos = obj.canvas.width - max_pad_left_over_width;
if(boundingRect.top < max_top_pos) {
obj.setTop(max_top_pos);
}
if(boundingRect.left < max_left_pos){
obj.setLeft(max_left_pos);
}
if(boundingRect.top > max_bottom_pos) {
obj.setTop(max_bottom_pos);
}
if(boundingRect.left > max_right_pos) {
obj.setLeft(max_right_pos);
}
});
I've created an example: https://jsfiddle.net/krio/wg0aL8ef/24/
Notice how when no rotation (angle) is applied you cannot force the image to leave the visible canvas. Now, add some rotation to the image and move it around. How can I get the rotated image to also always stay within view? Thanks.
Fabric.js provides with two object properties isContainedWithinRect and intersectsWithRect which we can use to solve the problem.
An object should not be allowed to go outside the canvas boundaries. This can be achieved if we somehow manage to make sure that the object is either inside the canvas, or at least intersects with the canvas boundaries.
For this case, since you want some portion of the image inside the canvas, we will take a rect smaller than the canvas instead of canvas boundaries to be the containing rect of the image.
Using the properties mentioned to make sure that the above two conditions are satisfied after any movement. Here's how the code looks like:
var canvas = new fabric.Canvas('c');
var imgElement = document.getElementById('my-img');
var imgInstance = new fabric.Image(imgElement, {
top: 0,
left: 0
});
imgInstance.setScaleX(0.6);
imgInstance.setScaleY(0.6);
canvas.add(imgInstance);
var prevLeft = 0,
prevTop = 0;
var max_pad_left_over_width = imgInstance.currentWidth * .08;
var max_pad_left_over_height = imgInstance.currentHeight * .08;
var max_top_pos = max_pad_left_over_height;
var max_bottom_pos = imgInstance.canvas.height - max_pad_left_over_height;
var max_left_pos = max_pad_left_over_width;
var max_right_pos = imgInstance.canvas.width - max_pad_left_over_width;
var pointTL = new fabric.Point(max_left_pos, max_top_pos);
var pointBR = new fabric.Point(max_right_pos, max_bottom_pos);
canvas.on('object:moving', function(e) {
var obj = e.target;
obj.setCoords();
if (!obj.intersectsWithRect(pointTL, pointBR) && !obj.isContainedWithinRect(pointTL, pointBR)) {
obj.setTop(prevTop);
obj.setLeft(prevLeft);
}
prevLeft = obj.left;
prevTop = obj.top;
});
Here's the link to the fiddle: https://jsfiddle.net/1ghvjxbk/ and documentation where these properties are mentioned.

PDFView.page.getViewport() not defined?

So I'm using the normal viewer.html and the following function included in a separate .js:
function selectedPosition()
{
var visible = PDFView.getVisiblePages();
var firstPage = visible.first;
var pageNumber = firstPage.id;
var currentPage = PDFView.pages[pageNumber - 1];
var selecion = window.getSelection();
var range = selecion.getRangeAt(0);
var rect = range.getBoundingClientRect();
var div = $('#viewer');
var viewport = currentPage.getViewport(div.width / currentPage.getViewport(1.0).width); // Uncaught TypeError: undefined is not a function
var point1 = viewport.convertToPdfPoint(rect.left - div.offset().left, rect.top - div.offset().top);
var point2 = viewport.convertToPdfPoint(rect.right - div.offset().left, rect.bottom - div.offset().top);
console.log(point1);
console.log(point2);
}
What the code should do, is calculate the rectangle points with respect to the pdf so that they are independent on scrolling and zoom.
The code is mostly copied from the viewer.js, so why is getViewport not defined?
Trying
console.log(currentPage.getViewport(1.0));
yields the same error. What am I missing?
I think you can use the "viewport" property of the PageView object that is returned by the call to PDFView.pages[pageNumber - 1]. For example:
function selectedPosition()
{
var visible = PDFView.getVisiblePages();
var firstPage = visible.first;
var pageNumber = firstPage.id;
var currentPage = PDFView.pages[pageNumber - 1];
...
//var viewport = currentPage.getViewport(div.width / currentPage.getViewport(1.0).width);
var viewport = currentPage.viewport; // Should return a reference to the viewport for the page
...
}

Update DIV content on click without directly knowing its ID

I am building 2D tile game map. Each tile is a div in 2D array (var tiles = []). Below is the function which builds a tile based on some arguments defined somewhere else:
function Tile(rnd, px, py, nid) {
var self = this;
var _types = ["grass","forest","hills","swamp","forest", "mountains"];
var height = 60;
var width = 60;
var tileID = nid // new numeric tile id
var id = "tile_"+px+py+rnd; // old tile ID
var x = px;
var y = py;
var type = _types[rnd];
var img = 'img/maptiles/'+type+'.png';
this.Draw = function() {
var div = $("<div class='tile'></div>");
div.attr('id',tileID).data('type',type).data('x',x).data('y',y);
div.get(0).tile = self;
div.css({top:height*y, left:width*x});
div.css({"background":"url('"+img+"')"});
div.appendTo('#map-content');
};
this.Alert = function() {
alert("Tile type: "+type+". Tile ID: "+tileID+" ");
};
this.Move = function(){ // moves player to available tile, in this case if player stands to a tile before the clicked one
alert("start move! Tile type: "+type+". Tile ID: "+tileID+" ");
if (playerPosition === tileID - 1) {
$("#player").remove();
$("#????")").append('<img id="player" src="Pictures/maptiles/player.png" />');
playerPosition = tileID;
alert("Player position now: " + playerPosition);
}
};
}
As a result I end up with m x n map made of divs each with a unique numeric ID starting with 1. I know that using numeric IDs for elements is(was?) frowned upon, though HTML5 specs do not actually prohibit this anymore.
Now what I want to do is to place a player icon (player.png) depending on player's position. Starting position is 1 and I am using tiles (aka divs) numeric IDs to calculate legal moves (can move only to bordering tiles).
When a tile is clicked this.Move is called. Its purpose is to check if player can move on a clicked tile (just one IF statement for now to test) and must remove player.png and redraw it on the clicked div.
Here I run into a problem since I need to somehow use the tileID (which is equal to Div ID) to tell browser to append the DIV which is belong clicked (as I obviously do not write a function for every div on the field). I think that since I can get the DIV id on click I can use it somehow.
I tried to experiment with this.div.id:eq("+tileID+") but with no luck.
UPD:
Adding click handler as requested. Note this is within var Map, which is responsible for building the map, rendering and handling some user input:
var Map = new function() {
var maxHorz = 20;
var maxVert = 5;
var tiles = [];
this.init = function() {
for(var i=0; i<maxVert; i++) {
tiles[i] = [];
for(var j=0; j<maxHorz; j++) {
tiles[i][j] = new Tile(Math.random()*6|0, j, i, tileID++);
}
}
Render();
Setup();
};
this.GetMap = function() {
return tiles;
};
var Render = function() {
$.each(tiles, function(k,v){
$.each(v, function(k,t){
t.Draw();
});
});
};
var Setup = function(){
$('#map-content').on('click','div.tile', function(e){
//var tile = tiles[$(this).data('y')][$(this).data('x')];
this.tile.Move();
});
}
this.Redraw = function(x,y) {
};
}
Map.init();
The answer is found, finally :)
$('#player').detach().appendTo($('#'+tileID))

context is not defined, javascript

function moveCamera() {
var canvas_holder = document.getElementById("game");
var context = canvas_holder.getContext("2d");
// camera code
// START
// Viewport height
var eye = $("#container").height();
// canvas height
var can_height = $("#canvas").height();
// Initial pos. of car. Multiplied by 30 to convert it into pixels.
// CHECK BETTER FOR CONVERTION
var car_pos = (car.GetWorldCenter().y)*30;
var half_eye = (eye/2);
// Initial Settings of camera
if ((car_pos > half_eye) && (can_height - car_pos) > half_eye){
context.translate(-(car_pos-half_eye),0);
} else if ((can_height - car_pos) < half_eye){
context.translate(-(can_height - eye), 0);
}
var pos = new Array();
var length;
// initial position of object is stored
pos[0] = (car.GetWorldCenter().y)*30;
//move camera
//vector is defined which will store the current y-coordinate
var p = new b2Vec2();
p = (car.GetWorldCenter().y)*30;
pos.push(p);
var length = pos.length;
// in pexels. finds out the distance moved in one timestep
var s = (pos[length-1]-pos[length-2]);
// checks if object is not in eye/2. Thus can be translated
if ((half_eye < (can_height - p)) && (p > half_eye)){
// canvas is translated
context.translate(-s,0);
}
}
so i have the code above and i get a reference error context is not defined.
why is that? i mean i did wrote:
var canvas_holder = document.getElementById("game");
var context = canvas_holder.getContext("2d");
but it looks like it not working... why? what am i missing? can it be that .getContext is not written properly?

Categories