Highchart tooltip show nearest point - javascript

I have been trying to make highchart tooltip to show the nearest point incase the x-axis value aren't align in different series.
This is what I got so far
http://jsfiddle.net/Yw8hb/5/
Highcharts.wrap(Highcharts.Tooltip.prototype, 'refresh', function (proceed) {
var args = arguments,
points = args[1],
point = points[0],
chart = point.series.chart;
// Loop over all the series of the chart
Highcharts.each(chart.series, function(series) {
// This one already exist
if (series == point.series) return;
var current,
dist,
distance = Number.MAX_VALUE;
// Loop over all the points
Highcharts.each(series.points, function(p) {
// use the distance in X to determine the closest point
dist = Math.abs(p.x - point.x);
if (dist < distance) {
distance = dist;
current = p;
}
});
// Add the closest point to the array
points.push(current);
});
proceed.apply(this, [].slice.call(args, 1));
});
It seems to be working half way there however when you hover in some spot it shows duplicated series. I have spent hours trying to figure this out any help would be very appreciated.

Before insertion, check whether points array contains the current point in your refresh callback function.
// Add the closest point to the array
if(points.indexOf(current)==-1)
points.push(current);
Highcharts.wrap(Highcharts.Tooltip.prototype, 'refresh', function (proceed) {
var args = arguments,
points = args[1],
point = points[0],
chart = point.series.chart;
// Loop over all the series of the chart
Highcharts.each(chart.series, function(series) {
// This one already exist
if (series == point.series) return;
var current,
dist,
distance = Number.MAX_VALUE;
// Loop over all the points
Highcharts.each(series.points, function(p) {
// use the distance in X to determine the closest point
dist = Math.abs(p.x - point.x);
if (dist < distance) {
distance = dist;
current = p;
}
});
// Add the closest point to the array
if(points.indexOf(current)==-1)
points.push(current);
});
proceed.apply(this, [].slice.call(args, 1));
});
$('#container').highcharts({
tooltip: {
shared: true
},
xAxis: {
crosshair: {
color: '#F70000'
}
},
series: [{
data: [{
x: 0.0,
y: 1
}, {
x: 1.0,
y: 2
}, {
x: 2.0,
y: 3
}, {
x: 3.0,
y: 2
}, {
x: 4.0,
y: 1
}]
}, {
data: [{
x: 0.2,
y: 0
}, {
x: 1.2,
y: 1
}, {
x: 2.2,
y: 1
}, {
x: 3.2,
y: 1
}, {
x: 4.2,
y: 2
}]
}, {
data: [{
x: 0.2,
y: 5
}, {
x: 1.2,
y: 9
}, {
x: 2.2,
y: 4
}, {
x: 3.2,
y: 5
}, {
x: 4.2,
y: 3
}]
}]
});
#container {
min-width: 300px;
max-width: 800px;
height: 300px;
margin: 1em auto;
}
<script src="http://code.jquery.com/jquery-git.js"></script>
<script src="http://code.highcharts.com/highcharts.js"></script>
<script src="http://code.highcharts.com/modules/exporting.js"></script>
<div id="container"></div>

If you want to show visible series' in the tooltip only, change
// This one already exist
if (series == point.series) return;
to
// This one already exist
if (series == point.series || series.visible==false) return;
Thanks for you solution!!!

for constant order the tooltips
Highcharts.wrap(Highcharts.Tooltip.prototype, 'refresh', function (proceed) {
var args = arguments,
points = args[1],
point = points[0],
chart = point.series.chart;
// Loop over all the series of the chart
Highcharts.each(chart.series, function (series) {
// This one already exist
if (series == point.series || series.visible == false)
return;
var current,
dist,
distance = Number.MAX_VALUE;
// Loop over all the points
Highcharts.each(series.points, function (p) {
// use the distance in X to determine the closest point
dist = Math.abs(p.x - point.x);
if (dist < distance) {
distance = dist;
current = p;
return;
}
});
// Add the closest point to the array
if (points.indexOf(current) == -1)
points.push(current);
});
// for not changing the tooltip series order
var tt = [].slice.call(args, 1);
tt[0].sort(function (a, b) {
if (a.color < b.color)
return -1;
if (a.color > b.color)
return 1;
return 0;
});
proceed.apply(this, tt);
});

Don't forget tooltip option shared!
options = {
tooltip: {
shared: true,
....

Related

Calculate which of coords in object is closest to choosen coords

I have some objects in array with coordinates.
var players = [
{x:100, y:100, pid:1},
{x:-100, y:200, pid:2},
{x:600, y:1200, pid:3}
]
var mousepos = {x:50, y:0}
var selected = null
And I want to calculate which of these objects have closest coordinates to mousepos to select it pid
selected = pid of closest coords to mousepos
if anyone could give me easy done code?
Mate here is the answer. Use the distance formula to calculate the distance between points. One with the smallest distance is the closest point.
Fomula :- Sqrt((x2-x1)2 + (y2-y1)2)
It seems you are trying to calculate Euclidean Distance. If so then you can use Math build in object. Once the distance is calculated then you can return the index. Using this index retrieve the he object from the players
var players = [{
x: 100,
y: 100,
pid: 1
},
{
x: -100,
y: 200,
pid: 2
},
{
x: 600,
y: 1200,
pid: 3
}
]
var mousepos = {
x: 50,
y: 0
}
function calculateEuclidean() {
let selectedPid = 0;
players.forEach(function(item, index) {
let distance = Math.sqrt(Math.pow((item.x - mousepos.x), 2) + Math.pow((item.y - mousepos.y), 2))
if (index === 0) {
selectedPid = index
} else if (distance < selectedPid) {
selectedPid = index;
}
})
return players[selectedPid];
}
console.log(calculateEuclidean())
Working example given by random from discord
let closestPlayer = undefined;
let closestDist = undefined;
for (let i = 0; i < players.length; i++) {
let player = players[i];
let distance = Math.hypot(player.x - mousepos.x, player.y - mousepos.y);
if (closestPlayer == undefined || distance <= closestDist) {
closestPlayer = player;
closestDist = distance;
}
}
console.log(closestPlayer)

Highcharts solidgauge default tooltip position

https://jsfiddle.net/7cbymps6/
tooltip: {
positioner: function () {
if(true) {
return {
x: this.chart.plotWidth / 1.5,
y: this.chart.plotHeight / 2
};
} else {
return {
/*x: defaultX ??,
y: defaultY ?? */
};
}
}
},
In else statement I want the tooltip position as it is. But in case of no return statement, it gives error. If I return an empty object, this time it puts the tooltip on the upper left hand side of the chart. How kann I return previous x and y values? Thanks
You have to compute the default position using callback properties: point, labelWidth and labelHeight like that:
tooltip: {
positioner: function(labelWidth, labelHeight, point) {
if (false) {
return {
x: this.chart.plotWidth / 1.5,
y: this.chart.plotHeight / 2
};
} else {
return {
x: point.plotX + this.chart.plotLeft - labelWidth / 2,
y: point.plotY + this.chart.plotTop - labelHeight - 10
};
}
}
}
Demo:
https://jsfiddle.net/BlackLabel/jo0eka18/

Javascript, find first empty spot in grid

I have a grid with items inside of it with x and y co-orditantes. I am trying to write a function (with lodash) to determine where is the first empty spot where the top most left most spot is the first position.
I am trying to do this by iterating over each spot until I find the first empty spot. It is only a 2 column layout so I work through them in a pattern like so - x: 0, y:0 -> x:1, y:0 -> x:0, y:1 -> x:1, y:1 ... and then checking all the items along the way to see if there is not a match, so I then know if there is an opening. My attempt looks like so :
function fillEmptySpace(isFilled, startX, startY) {
if (!isFilled) {
_.forEach(items, function(item, i) {
if (!_.isMatch(item, {
'x': startX
}) && !_.isMatch(item, {
'y': startY
})
) {
console.log("empty spot at", startX, startY);
isFilled = true;
} else if (!_.isMatch(item, {
'x': startX + 1
}) && !_.isMatch(item, {
'y': startY
})) {
console.log("empty spot at", startX + 1, startY);
isFilled = true;
}
});
startY += 1;
fillEmptySpace(isFilled, startX, startY);
}
}
fillEmptySpace(false, 0, 0);
The data looks like so :
var items = [{
i: 'a',
x: 0,
y: 0,
w: 1,
h: 1,
maxW: 2
}, {
i: 'b',
x: 1,
y: 4,
w: 1,
h: 1,
maxW: 2
}, {
i: 'c',
x: 0,
y: 1,
w: 1,
h: 1,
maxW: 2
}, {
i: 'd',
x: 0,
y: 2,
w: 1,
h: 1,
maxW: 2
}];
And here is the fiddle I have been fooling around in : https://jsfiddle.net/alexjm/ugpy13xd/38/
I can't seem to get this logic quite right, I am not sure a this point where I am getting it wrong. Any input would be greatly appreciated!
Just as a note : with the provided data it should identify the first empty space as x:1, y:0, however right now it is saying empty spot at 0 0, which cannot be correct. Thanks!
When it comes to 2D arrays, the 1D index can be calculated with x + y * width. If we then sort the 1D indexes, we can create an O(nlogn) solution:
function findEmptySpace(grid, width) {
var index = _(grid)
.map(function(p) { return p.x + p.y * width })
.sortBy()
.findIndex(_.negate(_.eq));
if (index < 0) index = grid.length;
return {
x: index % width,
y: index / width >> 0 // ">> 0" has the same result as "Math.floor"
};
}
var items = [{x:0,y:0},{x:0,y:4},{x:0,y:1},{x:0,y:2}];
function findEmptySpace(grid, width) {
var index = _(grid)
.map(function(p) { return p.x + p.y * width; })
.sortBy()
.findIndex(_.negate(_.eq));
if (index < 0) index = grid.length;
return {
x: index % width,
y: index / width >> 0 // ">> 0" has the same result as "Math.floor"
};
}
document.getElementById('btn').onclick = function() {
var space = findEmptySpace(items, 2);
items.push(space);
console.log(space);
};
#btn { font-size: 14pt }
<script src="https://cdn.jsdelivr.net/lodash/4.13.1/lodash.min.js"></script>
<button id="btn">Fill the Empty Space</button>
If you pre-sort the array, the solution would be worst-case O(n).
May I suggest checking to see if the point exists vs checking to see if it doesn't. Iterate over each item in the list to see if it exists if it does set a flag, then increment positions through your grid. Keep in mind this will not account for coords less than your intial value of "startY". Consider the following code:
function findEmptySpace(startX, startY) {
var isFilled = false;
_.forEach(items, function(item, i) {
if (_.isMatch(item, { 'x': startX }) && _.isMatch(item, { 'y': startY }) {
// this spot is filled check next x
isFilled = true;
continue;
}
}
if (isFilled == true) {
// we need to recursively call our function but I don't know the value of x
(startX == 0) ? findEmptySpace(1, startY): findEmptySpace(0, startY + 1);
} else {
console.log("Congrats, we found a spot", startX, startY);
}
}
It looks like you're always going to find a match at 0,0 since your logic is finding if there is any item in the list that is not on 0,0, instead of if there is an item in the list on 0,0
What you really want to do is stop checking once you've found an item in the current x,y (and, additionally, check both x and y in your isMatch). You can use your existing routine and your existing isFilled check:
function fillEmptySpace(isFilled, startX, startY) {
_.forEach(items, function(item, i) {
if (!isFilled) {
if (!_.isMatch(item, {'x': startX, 'y': startY})) {
console.log("empty spot at", startX, startY);
isFilled = true;
} else if (!_.isMatch(item, {'x': startX + 1, 'y': startY})) {
console.log("empty spot at", startX + 1, startY);
isFilled = true;
}
}
});
if (!isFilled) {
startY += 1;
fillEmptySpace(isFilled, startX, startY);
}
}

How can I evenly distribute ticks when using maxTicksLimit?

I made a line chart using Chart.js version 2.1.3.
var canvas = $('#gold_chart').get(0);
var ctx = canvas.getContext('2d');
var fillPatternGold = ctx.createLinearGradient(0, 0, 0, canvas.height);
fillPatternGold.addColorStop(0, '#fdca55');
fillPatternGold.addColorStop(1, '#ffffff');
var goldChart = new Chart(ctx, {
type: 'line',
animation: false,
data: {
labels: dates,
datasets: [{
label: '',
data: prices,
pointRadius: 0,
borderWidth: 1,
borderColor: '#a97f35',
backgroundColor: fillPatternGold
}]
},
title: {
position: 'bottom',
text: '\u7F8E\u5143 / \u76CE\u53F8'
},
options: {
legend: {
display: false
},
tooltips: {
callback: function(tooltipItem) {
return tooltipItem.yLabel;
}
},
scales: {
xAxes: [{
ticks: {
maxTicksLimit: 8
}
}]
}
}
});
The output is as follow:
As you can see, I limited the maximum count of ticks to 8 via maxTicksLimit. However, the distribution is not even. How can I make the ticks distribute evenly?
p.s. there are always 289 records in the dataset, and the data is recorded every 5 minutes. Sample values of prices variable are:
[
{"14:10", 1280.3},
{"14:15", 1280.25},
{"14:20", 1282.85}
]
I tried different values of maxTicksLimit, and the results are still not distributed evenly.
Chart.js uses an integral skipRatio (to figure out how many labels to skip). With Chart.js v2.1.x, you can write your own plugin to use a fractional skipRatio
Preview
Script
Chart.pluginService.register({
afterUpdate: function (chart) {
var xScale = chart.scales['x-axis-0'];
if (xScale.options.ticks.maxTicksLimit) {
// store the original maxTicksLimit
xScale.options.ticks._maxTicksLimit = xScale.options.ticks.maxTicksLimit;
// let chart.js draw the first and last label
xScale.options.ticks.maxTicksLimit = (xScale.ticks.length % xScale.options.ticks._maxTicksLimit === 0) ? 1 : 2;
var originalXScaleDraw = xScale.draw
xScale.draw = function () {
originalXScaleDraw.apply(this, arguments);
var xScale = chart.scales['x-axis-0'];
if (xScale.options.ticks.maxTicksLimit) {
var helpers = Chart.helpers;
var tickFontColor = helpers.getValueOrDefault(xScale.options.ticks.fontColor, Chart.defaults.global.defaultFontColor);
var tickFontSize = helpers.getValueOrDefault(xScale.options.ticks.fontSize, Chart.defaults.global.defaultFontSize);
var tickFontStyle = helpers.getValueOrDefault(xScale.options.ticks.fontStyle, Chart.defaults.global.defaultFontStyle);
var tickFontFamily = helpers.getValueOrDefault(xScale.options.ticks.fontFamily, Chart.defaults.global.defaultFontFamily);
var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily);
var tl = xScale.options.gridLines.tickMarkLength;
var isRotated = xScale.labelRotation !== 0;
var yTickStart = xScale.top;
var yTickEnd = xScale.top + tl;
var chartArea = chart.chartArea;
// use the saved ticks
var maxTicks = xScale.options.ticks._maxTicksLimit - 1;
var ticksPerVisibleTick = xScale.ticks.length / maxTicks;
// chart.js uses an integral skipRatio - this causes all the fractional ticks to be accounted for between the last 2 labels
// we use a fractional skipRatio
var ticksCovered = 0;
helpers.each(xScale.ticks, function (label, index) {
if (index < ticksCovered)
return;
ticksCovered += ticksPerVisibleTick;
// chart.js has already drawn these 2
if (index === 0 || index === (xScale.ticks.length - 1))
return;
// copy of chart.js code
var xLineValue = this.getPixelForTick(index);
var xLabelValue = this.getPixelForTick(index, this.options.gridLines.offsetGridLines);
if (this.options.gridLines.display) {
this.ctx.lineWidth = this.options.gridLines.lineWidth;
this.ctx.strokeStyle = this.options.gridLines.color;
xLineValue += helpers.aliasPixel(this.ctx.lineWidth);
// Draw the label area
this.ctx.beginPath();
if (this.options.gridLines.drawTicks) {
this.ctx.moveTo(xLineValue, yTickStart);
this.ctx.lineTo(xLineValue, yTickEnd);
}
// Draw the chart area
if (this.options.gridLines.drawOnChartArea) {
this.ctx.moveTo(xLineValue, chartArea.top);
this.ctx.lineTo(xLineValue, chartArea.bottom);
}
// Need to stroke in the loop because we are potentially changing line widths & colours
this.ctx.stroke();
}
if (this.options.ticks.display) {
this.ctx.save();
this.ctx.translate(xLabelValue + this.options.ticks.labelOffset, (isRotated) ? this.top + 12 : this.options.position === "top" ? this.bottom - tl : this.top + tl);
this.ctx.rotate(helpers.toRadians(this.labelRotation) * -1);
this.ctx.font = tickLabelFont;
this.ctx.textAlign = (isRotated) ? "right" : "center";
this.ctx.textBaseline = (isRotated) ? "middle" : this.options.position === "top" ? "bottom" : "top";
this.ctx.fillText(label, 0, 0);
this.ctx.restore();
}
}, xScale);
}
};
}
},
});
Fiddle - http://jsfiddle.net/bh63pe1v/
A simpler solution until this is permanently fixed by the Chart JS contributors is to include a decimal in maxTicksLimit.
For example:
maxTicksLimit: 8,
produces a huge gap at the end.
maxTicksLimit: 8.1,
Does not produce a huge gap at the end.
Depending on what you want to set your maxTicksLimit to, you need to play around with different decimals to see which one produces the best result.
Just do this:
yAxes: [{
ticks: {
stepSize: Math.round((Math.max.apply(Math, myListOfyValues) / 10)/5)*5,
beginAtZero: true,
precision: 0
}
}]
10 = the number of ticks
5 = rounds tick values to the nearest 5 - all y values will be incremented evenly
Similar will work for xAxes too.

Generating a Smooth Random Trend (Random Walk) in JavaScript

I am looking for a JavaScript implementation of a random walk/random trend algorithm. I need something that will stick with a trend (so, just plain random deltas are out) while still staying within some specified boundaries.
I tried writing something off the top of my head by choosing numbers based on a weighted average (the weight was calculated using the Gaussian function) and ended up with a slightly smoother line (not good enough). I then took a less direct approach and tried searching on the internet, and although I found a few outlines, there was nothing specific enough that I understood.
As it turns out (I was not aware of this originally), it seems there is already a family of algorithms that use the Gaussian equation to make a random trend. But, despite hours of searching, I couldn't find much more than abstract equations that were no use to me. The best that I could find was this blog where he shows a picture of random data like I'm looking for. He lists equations, but I have no idea what those actually are supposed to mean (to me, it doesn't seem like it's even a full solution).
What algorithms are already out there (JavaScript or C-like implementations preferably) to generate data like this?
Here is what I came up with from reading the blog that you linked. As far as I can tell, this is what the author did for his first graph.
CSS
#container {
min-width: 310px;
height: 400px;
margin: 0 auto;
}
HTML
<div id="container"></div>
Javascript
Box–Muller transform to generate Gaussian Random Numbers
var boxMullerRandom = (function () {
var phase = 0,
RAND_MAX,
array,
random,
x1, x2, w, z;
if (crypto && typeof crypto.getRandomValues === 'function') {
RAND_MAX = Math.pow(2, 32) - 1;
array = new Uint32Array(1);
random = function () {
crypto.getRandomValues(array);
return array[0] / RAND_MAX;
};
} else {
random = Math.random;
}
return function () {
if (!phase) {
do {
x1 = 2.0 * random() - 1.0;
x2 = 2.0 * random() - 1.0;
w = x1 * x1 + x2 * x2;
} while (w >= 1.0);
w = Math.sqrt((-2.0 * Math.log(w)) / w);
z = x1 * w;
} else {
z = x2 * w;
}
phase ^= 1;
return z;
}
}());
Random Walk generator
function randomWalk(steps, randFunc) {
steps = steps >>> 0 || 100;
if (typeof randFunc !== 'function') {
randFunc = boxMullerRandom;
}
var points = [],
value = 0,
t;
for (t = 0; t < steps; t += 1) {
value += randFunc();
points.push([t, value]);
}
return points;
}
Helper function to get the Y values from the Random Walk points
function getYValues(points) {
return points.map(function (point) {
return point[1];
});
}
Helper function to generate X plots for the graph
function generatePlots(howMany) {
howMany = howMany >>> 0 || 10;
var plots = [],
index;
for (index = 0; index < howMany; index += 1) {
plots.push({
name: 'plot' + index,
data: getYValues(randomWalk())
});
}
return plots;
}
Graph the results, uses jQuery and highcharts.js
$('#container').highcharts({
title: {
text: 'Random Walk',
x: -20 //center
},
subtitle: {
text: 'Random Walk',
x: -20
},
xAxis: {
type: 'linear'
},
yAxis: {
title: {
text: 'Value'
},
plotLines: [{
value: 0,
width: 1,
color: '#808080'
}]
},
tooltip: {
valueSuffix: ' units'
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'middle',
borderWidth: 0
},
series: generatePlots(10)
});
On jsFiddle

Categories

Resources