Related
I'm using MatterJs for a physics based game and have not found a solution for the problem of preventing bodies being force-dragged by the mouse through other bodies. If you drag a body into another body, the body being dragged can force itself into and through the other body. I'm looking for a reliable way to prevent them from intersecting. You can observe this effect in any MatterJS demo by selecting a body with the mouse, and trying to force it through another body. Here is a typical example:
https://brm.io/matter-js/demo/#staticFriction
Unfortunately this breaks any games or simulations depending on drag-and-drop.
I have attempted numerous solutions, such as breaking the mouse constraint when a collision occurs, or reducing constraint stiffness, but nothing which works reliably.
Any suggestions welcome!
I think that the best answer here would be a significant overhaul to the Matter.Resolver module to implement predictive avoidance of physical conflicts between any bodies. Anything short of that is guaranteed to fail under certain circumstances. That being said here are two "solutions" which, in reality, are just partial solutions. They are outlined below.
Solution 1 (Update)
This solution has several advantages:
It is more concise than Solution 2
It creates a smaller computational footprint than Solution 2
The drag behavior is not interrupted the way it is in Solution 2
It can be non-destructively combined with Solution 2
The idea behind this approach is to resolve the paradox of what happens "when an unstoppable force meets an immovable object" by rendering the force stoppable. This is enabled by the Matter.Event beforeUpdate, which allows the absolute velocity and impulse (or rather positionImpulse, which isn't really physical impulse) in each direction to be constrained to within user-defined bounds.
window.addEventListener('load', function() {
var canvas = document.getElementById('world')
var mouseNull = document.getElementById('mouseNull')
var engine = Matter.Engine.create();
var world = engine.world;
var render = Matter.Render.create({ element: document.body, canvas: canvas,
engine: engine, options: { width: 800, height: 800,
background: 'transparent',showVelocity: true }});
var body = Matter.Bodies.rectangle(400, 500, 200, 60, { isStatic: true}),
size = 50, counter = -1;
var stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6,
0, 0, function(x, y) {
return Matter.Bodies.rectangle(x, y, size * 2, size, {
slop: 0, friction: 1, frictionStatic: Infinity });
});
Matter.World.add(world, [ body, stack,
Matter.Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
Matter.Bodies.rectangle(400, 600, 800, 50, { isStatic: true }),
Matter.Bodies.rectangle(800, 300, 50, 600, { isStatic: true }),
Matter.Bodies.rectangle(0, 300, 50, 600, { isStatic: true })
]);
Matter.Events.on(engine, 'beforeUpdate', function(event) {
counter += 0.014;
if (counter < 0) { return; }
var px = 400 + 100 * Math.sin(counter);
Matter.Body.setVelocity(body, { x: px - body.position.x, y: 0 });
Matter.Body.setPosition(body, { x: px, y: body.position.y });
if (dragBody != null) {
if (dragBody.velocity.x > 25.0) {
Matter.Body.setVelocity(dragBody, {x: 25, y: dragBody.velocity.y });
}
if (dragBody.velocity.y > 25.0) {
Matter.Body.setVelocity(dragBody, {x: dragBody.velocity.x, y: 25 });
}
if (dragBody.positionImpulse.x > 25.0) {
dragBody.positionImpulse.x = 25.0;
}
if (dragBody.positionImpulse.y > 25.0) {
dragBody.positionImpulse.y = 25.0;
}
}
});
var mouse = Matter.Mouse.create(render.canvas),
mouseConstraint = Matter.MouseConstraint.create(engine, { mouse: mouse,
constraint: { stiffness: 0.1, render: { visible: false }}});
var dragBody = null
Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
dragBody = event.body;
});
Matter.World.add(world, mouseConstraint);
render.mouse = mouse;
Matter.Engine.run(engine);
Matter.Render.run(render);
});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.js"></script>
In the example I am restricting the velocity and positionImpulse in x and y to a maximum magnitude of 25.0. The result is shown below
As you can see, it is possible to be quite violent in dragging the bodies and they will not pass through one another. This is what sets this approach apart from others: most other potential solutions fail when the user is sufficiently violent with their dragging.
The only shortcoming I have encountered with this method is that it is possible to use a non-static body to hit another non-static body hard enough to give it sufficient velocity to the point where the Resolver module will fail to detect the collision and allow the second body to pass through other bodies. (In the static friction example the required velocity is around 50.0, I've only managed to do this successfully one time, and consequently I do not have an animation depicting it).
Solution 2
This is an additional solution, fair warning though: it is not straightforward.
In broad terms the way this works is to check if the body being dragged, dragBody, has collided with a static body and if the mouse has since moved too far without dragBody following. If it detects that the separation between the mouse and dragBody has become too large it removes the Matter.js mouse.mousemove event listener from mouse.element and replaces it with a different mousemove function, mousemove(). This function checks if the mouse has returned to within a given proximity of the center of the body. Unfortunately I couldn't get the built-in Matter.Mouse._getRelativeMousePosition() method to work properly so I had to include it directly (someone more knowledgeable than me in Javascript will have to figure that one out). Finally, if a mouseup event is detected it switches back to the normal mousemove listener.
window.addEventListener('load', function() {
var canvas = document.getElementById('world')
var mouseNull = document.getElementById('mouseNull')
var engine = Matter.Engine.create();
var world = engine.world;
var render = Matter.Render.create({ element: document.body, canvas: canvas,
engine: engine, options: { width: 800, height: 800,
background: 'transparent',showVelocity: true }});
var body = Matter.Bodies.rectangle(400, 500, 200, 60, { isStatic: true}),
size = 50, counter = -1;
var stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6,
0, 0, function(x, y) {
return Matter.Bodies.rectangle(x, y, size * 2, size, {
slop: 0.5, friction: 1, frictionStatic: Infinity });
});
Matter.World.add(world, [ body, stack,
Matter.Bodies.rectangle(400, 0, 800, 50, { isStatic: true }),
Matter.Bodies.rectangle(400, 600, 800, 50, { isStatic: true }),
Matter.Bodies.rectangle(800, 300, 50, 600, { isStatic: true }),
Matter.Bodies.rectangle(0, 300, 50, 600, { isStatic: true })
]);
Matter.Events.on(engine, 'beforeUpdate', function(event) {
counter += 0.014;
if (counter < 0) { return; }
var px = 400 + 100 * Math.sin(counter);
Matter.Body.setVelocity(body, { x: px - body.position.x, y: 0 });
Matter.Body.setPosition(body, { x: px, y: body.position.y });
});
var mouse = Matter.Mouse.create(render.canvas),
mouseConstraint = Matter.MouseConstraint.create(engine, { mouse: mouse,
constraint: { stiffness: 0.2, render: { visible: false }}});
var dragBody, overshoot = 0.0, threshold = 50.0, loc, dloc, offset,
bodies = Matter.Composite.allBodies(world), moveOn = true;
getMousePosition = function(event) {
var element = mouse.element, pixelRatio = mouse.pixelRatio,
elementBounds = element.getBoundingClientRect(),
rootNode = (document.documentElement || document.body.parentNode ||
document.body),
scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset :
rootNode.scrollLeft,
scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset :
rootNode.scrollTop,
touches = event.changedTouches, x, y;
if (touches) {
x = touches[0].pageX - elementBounds.left - scrollX;
y = touches[0].pageY - elementBounds.top - scrollY;
} else {
x = event.pageX - elementBounds.left - scrollX;
y = event.pageY - elementBounds.top - scrollY;
}
return {
x: x / (element.clientWidth / (element.width || element.clientWidth) *
pixelRatio) * mouse.scale.x + mouse.offset.x,
y: y / (element.clientHeight / (element.height || element.clientHeight) *
pixelRatio) * mouse.scale.y + mouse.offset.y
};
};
mousemove = function() {
loc = getMousePosition(event);
dloc = dragBody.position;
overshoot = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5 - offset;
if (overshoot < threshold) {
mouse.element.removeEventListener("mousemove", mousemove);
mouse.element.addEventListener("mousemove", mouse.mousemove);
moveOn = true;
}
}
Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
dragBody = event.body;
loc = mouse.position;
dloc = dragBody.position;
offset = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5;
Matter.Events.on(mouseConstraint, 'mousemove', function(event) {
loc = mouse.position;
dloc = dragBody.position;
for (var i = 0; i < bodies.length; i++) {
overshoot = ((loc.x - dloc.x)**2 + (loc.y - dloc.y)**2)**0.5 - offset;
if (bodies[i] != dragBody &&
Matter.SAT.collides(bodies[i], dragBody).collided == true) {
if (overshoot > threshold) {
if (moveOn == true) {
mouse.element.removeEventListener("mousemove", mouse.mousemove);
mouse.element.addEventListener("mousemove", mousemove);
moveOn = false;
}
}
}
}
});
});
Matter.Events.on(mouseConstraint, 'mouseup', function(event) {
if (moveOn == false){
mouse.element.removeEventListener("mousemove", mousemove);
mouse.element.addEventListener("mousemove", mouse.mousemove);
moveOn = true;
}
});
Matter.Events.on(mouseConstraint, 'enddrag', function(event) {
overshoot = 0.0;
Matter.Events.off(mouseConstraint, 'mousemove');
});
Matter.World.add(world, mouseConstraint);
render.mouse = mouse;
Matter.Engine.run(engine);
Matter.Render.run(render);
});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.js"></script>
After applying the event listener switching scheme the bodies now behave more like this
I have tested this fairly thoroughly, but I can't guarantee it will work in every case. It also bears noting that the mouseup event is not detected unless the mouse is within the canvas when it occurs - but this is true for any Matter.js mouseup detection so I didn't try to fix that.
If the velocity is sufficiently large, Resolver will fail to detect any collision, and since it lacks predictive prevention of this flavor of physical conflict, will allow the body to pass through, as shown here.
This can be resolved by combining with Solution 1.
One last note here, it is possible to apply this to only certain interactions (e.g. those between a static and a non-static body). Doing so is accomplished by changing
if (bodies[i] != dragBody && Matter.SAT.collides(bodies[i], dragBody).collided == true) {
//...
}
to (for e.g. static bodies)
if (bodies[i].isStatic == true && bodies[i] != dragBody &&
Matter.SAT.collides(bodies[i], dragBody).collided == true) {
//...
}
Failed solutions
In case any future users come across this question and find both solutions insufficient for their use case, here are some of the solutions I attempted which did not work. A guide of sorts for what not to do.
Calling mouse.mouseup directly: object deleted immediately.
Calling mouse.mouseup via Event.trigger(mouseConstraint, 'mouseup', {mouse: mouse}): overridden by Engine.update, behavior unchanged.
Making the dragged object temporarily static: object deleted on returning to non-static (whether via Matter.Body.setStatic(body, false) or body.isStatic = false).
Setting the force to (0,0) via setForce when approaching conflict: object can still pass through, would need to be implemented in Resolver to actually work.
Changing mouse.element to a different canvas via setElement() or by mutating mouse.element directly: object deleted immediately.
Reverting object to last 'valid' position: still allows pass through,
Change behavior via collisionStart: inconsistent collision detection still permits pass through with this method
I would have managed the feature in another way:
No "drag" (so no continuos align of dragpoint with offset Vs dragged object)
On mouseDown the mouse pointer position give an oriented velocity vector for object to follow
On mouseUp reset your velocity vector
Let the matter simulation do the rest
To control collision when dragged you need to utilize collision filter and events.
Create bodies with default collision filter mask 0x0001. Add catch startdrag and enddrag events and set different body collision filter category to temporarily avoid collisions.
Matter.Events.on(mouseConstraint, 'startdrag', function(event) {
event.body.collisionFilter.category = 0x0008; // move body to new category to avoid collision
});
Matter.Events.on(mouseConstraint, 'enddrag', function(event) {
event.body.collisionFilter.category = 0x0001; // return body to default category to activate collision
});
window.addEventListener('load', function () {
//Fetch our canvas
var canvas = document.getElementById('world');
//Setup Matter JS
var engine = Matter.Engine.create();
var world = engine.world;
var render = Matter.Render.create({
canvas: canvas,
engine: engine,
options: {
width: 800,
height: 800,
background: 'transparent',
wireframes: false,
showAngleIndicator: false
}
});
//Add a ball
const size = 50;
const stack = Matter.Composites.stack(350, 470 - 6 * size, 1, 6, 0, 0, (x, y) => {
return Matter.Bodies.rectangle(x, y, size * 2, size, {
collisionFilter: {
mask: 0x0001,
},
slop: 0.5,
friction: 1,
frictionStatic: Infinity,
});
});
Matter.World.add(engine.world, stack);
//Add a floor
var floor = Matter.Bodies.rectangle(250, 520, 500, 40, {
isStatic: true, //An immovable object
render: {
visible: false
}
});
Matter.World.add(world, floor);
//Make interactive
var mouseConstraint = Matter.MouseConstraint.create(engine, { //Create Constraint
element: canvas,
constraint: {
render: {
visible: false
},
stiffness: 0.8
}
});
Matter.World.add(world, mouseConstraint);
// add events to listen drag
Matter.Events.on(mouseConstraint, 'startdrag', function (event) {
event.body.collisionFilter.category = 0x0008; // move body to new category to avoid collision
});
Matter.Events.on(mouseConstraint, 'enddrag', function (event) {
event.body.collisionFilter.category = 0x0001; // return body to default category to activate collision
});
//Start the engine
Matter.Engine.run(engine);
Matter.Render.run(render);
});
<canvas id="world"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.10.0/matter.min.js"></script>
This seems to be related to issue 672 on their GitHub page which seems to suggest that this occurs due to a lack of Continuous Collision Detection (CCD).
An attempt to remedy this has been made and the code for it can be found here but the issue is still open so it looks like you might need to edit the engine to build CCD into it yourself.
I have a doughnut chart using Chart.js that displays login data for my app correctly, however I have modified the chart so that the total number of logins is displayed in text in the center cutout:
The problem I am running into is with the tooltips. When I hover over the light teal piece of the pie chart, if the chart is scaled smaller, the tooltip is overlapped by the text in the center, like this:
I want to be able to change the direction the tooltip extends out, so instead of it going towards the center, it moves away so that both the tooltip and the center analytic are visible, but I have yet to find a concise explanation on how to change tooltip positioning. Here is the code I have currently:
var loslogged = dataset[0][0].loslogged;
var realtorlogged = dataset[1][0].realtorlogged;
var borrowerlogged = dataset[2][0].borrowerlogged;
var totallogged = parseInt(loslogged) + parseInt(realtorlogged) + parseInt(borrowerlogged);
Chart.pluginService.register({
afterDraw: function (chart) {
if (chart.config.options.elements.center) {
var helpers = Chart.helpers;
var centerX = (chart.chartArea.left + chart.chartArea.right) / 2;
var centerY = (chart.chartArea.top + chart.chartArea.bottom) / 2;
var ctx = chart.chart.ctx;
ctx.save();
var fontSize = helpers.getValueOrDefault(chart.config.options.elements.center.fontSize, Chart.defaults.global.defaultFontSize);
var fontStyle = helpers.getValueOrDefault(chart.config.options.elements.center.fontStyle, Chart.defaults.global.defaultFontStyle);
var fontFamily = helpers.getValueOrDefault(chart.config.options.elements.center.fontFamily, Chart.defaults.global.defaultFontFamily);
var font = helpers.fontString(fontSize, fontStyle, fontFamily);
ctx.font = font;
ctx.fillStyle = helpers.getValueOrDefault(chart.config.options.elements.center.fontColor, Chart.defaults.global.defaultFontColor);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(chart.config.options.elements.center.text, centerX, centerY);
ctx.restore();
}
}
});
var loginChartData = {
labels: ["Loan Officers","Realtors","Borrowers"],
datasets: [{
label: "Number of Logins",
data: [loslogged, realtorlogged, borrowerlogged],
backgroundColor: [
"rgba(191, 25, 25, 0.75)",
"rgba(58, 73, 208, 0.75)",
"rgba(79, 201, 188, 0.75)"
],
borderColor: [
"rgba(255, 255, 255, 1)",
"rgba(255, 255, 255, 1)",
"rgba(255, 255, 255, 1)"
],
borderWidth: 4
}],
gridLines: {
display: false
}
};
var loginChartOptions = {
title: {
display: false
},
cutoutPercentage: 50,
elements: {
center: {
text: totallogged,
fontColor: '#000',
fontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
fontSize: 36,
fontStyle: 'bold'
}
}
};
var loginChart = document.getElementById('loginsChart').getContext('2d');
new Chart(loginChart, {
type: 'doughnut',
data: loginChartData,
options: loginChartOptions
});
It used to be a lot easier to reverse the tooltips in previous versions of chart.js (v2.3 and before). All you had to do was overwrite the determineAlignment tooltip method and reverse the logic.
However starting in v2.4, the functions that calculate the tooltip positions (including determineAlignment) were made private, so there is no longer a way to simply overwrite them (instead you have to duplicate them).
Here is a working reversed tooltip solution that unfortunately requires a lot of copy and paste from the chart.js source (this is required since the methods are private). The risk with this approach is that the underlying private functions could change in new releases at any time and your new reverse tooltip could break unexpectedly.
With that said, here is walk through of the implementation (with a codepen example at the bottom).
1) First, let's extend the Chart.Tooltip object and create a new Chart.ReversedTooltip object. We really only need to overwrite the update method since it performs all the positioning logic. In fact, this overwrite is just a straight copy and paste from the source because we actually only need to modify the private determineAlignment method which is called by update.
// create a new reversed tooltip. we must overwrite the update method which is
// where all the positioning occurs
Chart.ReversedTooltip = Chart.Tooltip.extend({
update: function(changed) {
var me = this;
var opts = me._options;
// Need to regenerate the model because its faster than using extend and it is necessary due to the optimization in Chart.Element.transition
// that does _view = _model if ease === 1. This causes the 2nd tooltip update to set properties in both the view and model at the same time
// which breaks any animations.
var existingModel = me._model;
var model = me._model = getBaseModel(opts);
var active = me._active;
var data = me._data;
var chartInstance = me._chartInstance;
// In the case where active.length === 0 we need to keep these at existing values for good animations
var alignment = {
xAlign: existingModel.xAlign,
yAlign: existingModel.yAlign
};
var backgroundPoint = {
x: existingModel.x,
y: existingModel.y
};
var tooltipSize = {
width: existingModel.width,
height: existingModel.height
};
var tooltipPosition = {
x: existingModel.caretX,
y: existingModel.caretY
};
var i, len;
if (active.length) {
model.opacity = 1;
var labelColors = [];
tooltipPosition = Chart.Tooltip.positioners[opts.position](active, me._eventPosition);
var tooltipItems = [];
for (i = 0, len = active.length; i < len; ++i) {
tooltipItems.push(createTooltipItem(active[i]));
}
// If the user provided a filter function, use it to modify the tooltip items
if (opts.filter) {
tooltipItems = tooltipItems.filter(function(a) {
return opts.filter(a, data);
});
}
// If the user provided a sorting function, use it to modify the tooltip items
if (opts.itemSort) {
tooltipItems = tooltipItems.sort(function(a, b) {
return opts.itemSort(a, b, data);
});
}
// Determine colors for boxes
helpers.each(tooltipItems, function(tooltipItem) {
labelColors.push(opts.callbacks.labelColor.call(me, tooltipItem, chartInstance));
});
// Build the Text Lines
model.title = me.getTitle(tooltipItems, data);
model.beforeBody = me.getBeforeBody(tooltipItems, data);
model.body = me.getBody(tooltipItems, data);
model.afterBody = me.getAfterBody(tooltipItems, data);
model.footer = me.getFooter(tooltipItems, data);
// Initial positioning and colors
model.x = Math.round(tooltipPosition.x);
model.y = Math.round(tooltipPosition.y);
model.caretPadding = helpers.getValueOrDefault(tooltipPosition.padding, 2);
model.labelColors = labelColors;
// data points
model.dataPoints = tooltipItems;
// We need to determine alignment of the tooltip
tooltipSize = getTooltipSize(this, model);
alignment = determineAlignment(this, tooltipSize);
// Final Size and Position
backgroundPoint = getBackgroundPoint(model, tooltipSize, alignment);
} else {
model.opacity = 0;
}
model.xAlign = alignment.xAlign;
model.yAlign = alignment.yAlign;
model.x = backgroundPoint.x;
model.y = backgroundPoint.y;
model.width = tooltipSize.width;
model.height = tooltipSize.height;
// Point where the caret on the tooltip points to
model.caretX = tooltipPosition.x;
model.caretY = tooltipPosition.y;
me._model = model;
if (changed && opts.custom) {
opts.custom.call(me, model);
}
return me;
},
});
2) As you can see, the update method uses a handful of private methods (e.g. getBaseModel, createTooltipItem, determineAlignment, etc.). In order for our update method to actually work, we have to provide an implementation for each of these methods. Here again is another copy and paste from the source. The only method that we need to modify however is the determineAlignment method. Here is the modified version that reverses the alignment logic.
// modified from source to reverse the position
function determineAlignment(tooltip, size) {
var model = tooltip._model;
var chart = tooltip._chart;
var chartArea = tooltip._chartInstance.chartArea;
var xAlign = 'center';
var yAlign = 'center';
// set caret position to top or bottom if tooltip y position will extend outsite the chart top/bottom
if (model.y < size.height) {
yAlign = 'top';
} else if (model.y > (chart.height - size.height)) {
yAlign = 'bottom';
}
var leftAlign, rightAlign; // functions to determine left, right alignment
var overflowLeft, overflowRight; // functions to determine if left/right alignment causes tooltip to go outside chart
var yAlign; // function to get the y alignment if the tooltip goes outside of the left or right edges
var midX = (chartArea.left + chartArea.right) / 2;
var midY = (chartArea.top + chartArea.bottom) / 2;
if (yAlign === 'center') {
leftAlign = function(x) {
return x >= midX;
};
rightAlign = function(x) {
return x < midX;
};
} else {
leftAlign = function(x) {
return x <= (size.width / 2);
};
rightAlign = function(x) {
return x >= (chart.width - (size.width / 2));
};
}
overflowLeft = function(x) {
return x - size.width < 0;
};
overflowRight = function(x) {
return x + size.width > chart.width;
};
yAlign = function(y) {
return y <= midY ? 'bottom' : 'top';
};
if (leftAlign(model.x)) {
xAlign = 'left';
// Is tooltip too wide and goes over the right side of the chart.?
if (overflowLeft(model.x)) {
xAlign = 'center';
yAlign = yAlign(model.y);
}
} else if (rightAlign(model.x)) {
xAlign = 'right';
// Is tooltip too wide and goes outside left edge of canvas?
if (overflowRight(model.x)) {
xAlign = 'center';
yAlign = yAlign(model.y);
}
}
var opts = tooltip._options;
return {
xAlign: opts.xAlign ? opts.xAlign : xAlign,
yAlign: opts.yAlign ? opts.yAlign : yAlign
};
};
3) Now that our new Chart.ReversedTooltip is complete, we need to use the plugin system to change the original tooltip to our reversed tooltip. We can do this using the afterInit plugin method.
Chart.plugins.register({
afterInit: function (chartInstance) {
// replace the original tooltip with the reversed tooltip
chartInstance.tooltip = new Chart.ReversedTooltip({
_chart: chartInstance.chart,
_chartInstance: chartInstance,
_data: chartInstance.data,
_options: chartInstance.options.tooltips
}, chartInstance);
chartInstance.tooltip.initialize();
}
});
After all that, we finally have reversed tooltips! Checkout a full working example at this codepen.
It's also worth mentioning that this approach is very brittle and, as I mentioned, can easily break overtime (on account of the copy and pasting required). Another option would be to just use a custom tooltip instead and position it wherever you desire on the chart.
Checkout this chart.js sample that shows how to setup and use a custom tooltip. You could go with this approach and just modify the positioning logic.
If you have a small tooltip label, you can use simple chart.js options to fix overlaps issue:
plugins: {
tooltip: {
xAlign: 'center',
yAlign: 'bottom'
}
}
I managed to solve the same by setting zIndex of Doughnut wrapper div to 1, settting the zIndex of text shown in the middle of Doughnut to -1, and canvas is transparent by default.
Hope this hels.
Using various posts/questions in SO as reference I created a scatter highchart jsfiddle
xAxis: {
opposite:true,
type: 'datetime',
gridLineWidth: 1,
gridLineDashStyle: 'ShortDot',
gridLineColor:'black',
alternateGridColor: 'lightgrey',
tickInterval: 3 * 30 * 24 * 3600 * 1000, // 1 quarter
labels: {
//align: "left",
//padding:200,
formatter: function () {
var s = "";
if (Highcharts.dateFormat('%b', this.value) == 'Jan') {
s = s + "Q1"
};
if (Highcharts.dateFormat('%b', this.value) == 'Apr') {
s = s + "Q2"
};
if (Highcharts.dateFormat('%b', this.value) == 'Jul') {
s = s + "Q3"
};
if (Highcharts.dateFormat('%b', this.value) == 'Oct') {
s = s + "Q4"
};
s = s + " " + Highcharts.dateFormat('%Y', this.value);
return s;
}
},
plotLines: [{
color: 'red', // Color value
value: now, // Value of where the line will appear
width: 2, // Width of the line
label: {
text: 'Now',
align: 'center',
verticalAlign: 'bottom',
y: +20,
rotation: 0
}
}]
},
But I'm struck with having the X-axis label positioned near the tick.
How to move to middle of the grid?
Is there anyway I can achieve the below?
I tried align, padding but didn't help. When the timeline increases I should still have the labels positioned in the middle.
should I do something with tickInterval? It might be a simple property I'm missing.
I found this link jsfiddle which addresses my concern but with 2 x-axis and I'm populating the data from a list.
I implemented Christopher Cortez' solution found here:
However, also changed to to fire on the highcharts load event, rather than the callback, and I've changed it to be recalled when the HighCharts redraw event is fired, so that they stay aligned when the page is resized.
$('#container').highcharts({
chart: {
defaultSeriesType: 'scatter',
events: {
load: centerLabels,
redraw: centerLabels
}
},
/* ... all the other options ...*/
});
Where
function centerLabels(chart) {
var $container = $(chart.target.container);
var axes = chart.target.axes;
var $labels = $container.find('.highcharts-axis-labels .timeline_label');
var $thisLabel, $nextLabel, thisXPos, nextXPos, delta, newXPos;
$labels.each(function () {
$thisLabel = $(this).parent('span');
thisXPos = parseInt($thisLabel.css('left'));
$nextLabel = $thisLabel.next();
// next position is either a label or the end of the axis
nextXPos = $nextLabel.length ? parseInt($nextLabel.css('left')) : axes[0].left + axes[0].width;
delta = (nextXPos - thisXPos) / 2.0;
newXPos = thisXPos + delta;
// remove the last label if it won't fit
if ($nextLabel.length || $(this).width() + newXPos < nextXPos) {
$thisLabel.css('left', newXPos + 'px');
} else {
$thisLabel.remove();
}
});
}
JSFiddle
I am working on an application where I am using FlotJS charting library to plot graphs and I have turned on the tooltips so that users can hover over points and can tell the content of each point on a particular date.
Like in this interactive graph example by flot, we can hover over the points to see the values of sin and cos.
I am using the Smart Admin Responsive Theme in my project and if you navigate to the demo of graphs they are offering, there is also a section on Flot Chart Demos
If you look closely on the Sin Chart which is second in order, it is similar to what I am doing. Hovering over the points presents tooltips to view the value of sin.
The problem I am facing is that if you hover over the last few points, the tooltip appears but it messes with the responsiveness of the page and is not visible per say.
How to go around this problem? Is there some way I can make the tooltip appear to the left of the mouse pointer when it is on the end of the page?
EDIT
Options Object
var layerOptions = {
xaxis : {
mode : "time"
},
legend : {
show : true,
noColumns : 1, // number of colums in legend table
labelFormatter : null, // fn: string -> string
labelBoxBorderColor : "#000", // border color for the little label boxes
container : null, // container (as jQuery object) to put legend in, null means default on top of graph
position : "ne", // position of default legend container within plot
margin : [0, 5], // distance from grid edge to default legend container within plot
backgroundColor : "#efefef", // null means auto-detect
backgroundOpacity : 0.4 // set to 0 to avoid background
},
tooltip : true,
tooltipOpts : {
content : "<b>%s</b> content on <b>%x</b> was <b>%y</b>",
dateFormat : "%y-%m-%d",
defaultTheme : false
},
grid : {
hoverable : true,
clickable : true
},
series: {
/*bars: {
show: true,
barWidth: 1000 * 60 * 60 * 24 * 30,
order: 1
}*/
}
};
Yes, you can set the position of the tooltip dynamically depending on window size. I use this in one of my flot pages to flip the tooltip to the left when near the right border:
var prevPoint = null;
$("#placeholder").bind("plothover", function (event, pos, item) {
if (item) {
if (prevPoint != item.dataIndex) {
prevPoint = item.dataIndex;
$("#tooltip").remove();
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(2),
text = item.series.label + ' content on ' + x + ' was ' + y;
showTooltip(item.pageX, item.pageY, text);
}
} else {
$("#tooltip").remove();
prevPoint = null;
}
});
function showTooltip(x, y, content) {
var cssParams = {
position: 'absolute',
display: 'none',
border: '1px solid #888888',
padding: '2px',
'background-color': '#eeeeee',
opacity: 0.8
};
if (x < 0.8 * windowWidth) {
cssParams.left = x + 3;
}
else {
cssParams.right = windowWidth - x + 6;
}
if (y < 0.8 * windowHeight) {
cssParams.top = y + 3;
}
else {
cssParams.bottom = windowHeight - y + 6;
}
$('<div id="tooltip">' + content + '</div>').css(cssParams).appendTo('body').fadeIn(100);
}
windowHeight and windowWidth are global variables which are updated in the resize event (using $(window).height()). You can also use the dimensions of a container element if you like.
You can remove the tooltip plugin and options when you use the given events/functions.
I'm doing a responsive pie chart which holds title in centered position inside it.I've used,
title: {
text: "",
margin: 0,
y:0,
x:0,
align: 'center',
verticalAlign: 'middle',
},
but it's not perfectly centered inside the chart.Any suggestions would be appreciated.Thank you in advance.
Here's the Link : http://jsfiddle.net/LHSey/128/
Better is remove title and use renderer which allows to add custom text, which can be repositioned each time (when you redraw chart). Only what you need is catch this event.
function addTitle() {
if (this.title) {
this.title.destroy();
}
var r = this.renderer,
x = this.series[0].center[0] + this.plotLeft,
y = this.series[0].center[1] + this.plotTop;
this.title = r.text('Series 1', 0, 0)
.css({
color: '#4572A7',
fontSize: '16px'
}).hide()
.add();
var bbox = this.title.getBBox();
this.title.attr({
x: x - (bbox.width / 2),
y: y
}).show();
}
chart:{
events: {
load: addTitle,
redraw: addTitle,
},
}
Example: http://jsfiddle.net/LHSey/129/
We can customize any properties of title using chart.setTitle() as shown below.I've added a title and set useHTML property to true.
chart: {
events: {
load: function () {
this.setTitle({text: 'Title',useHTML:true});
}
}
}