Tooltip issue on stacked line graph in flot graphs - javascript

I am using stacked line graph but I am facing issue with hover tooltips. There are some values which are 0. I just want to ignore tooltip on 0 value points because they override points with greater than 0 value.
I have tried eliminating 0 value points from data array but by doing that graph does not render correctly.
Please have a look at this:

When using the flot.tooltip plugin, you can set the content property to a function which returns a string for the tooltip or false for the case where you don't want to show a tooltip (see the documentation), something like this:
tooltipOpts: {
content: function (label, x, y, datapoint) {
if (y == 0) {
return false;
}
else {
// change this to get your desired format
return (new Date(x)).toString() + label + x;
}
},
When generating the tooltip manually with the plothover event, check the value before showing the tooltip, something like this:
$("#placeholder").bind("plothover", function (event, pos, item) {
// check if value is zero
if (item && item.datapoint[1] != 0) {
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(2);
// change this to get your desired format
$("#tooltip").html(x + item.series.label + y)
.css({top: item.pageY + 5, left: item.pageX + 5}).fadeIn(200);
} else {
$("#tooltip").hide();
}
});

I have analyzed the code base and here are the changes which I have purposed.
https://github.com/flot/flot/pull/1447/files
## -607,7 +607,8 ## Licensed under the MIT license.
clickable: false,
hoverable: false,
autoHighlight: true, // highlight in case mouse is near
- mouseActiveRadius: 10 // how far the mouse can be away to activate an item
+ mouseActiveRadius: 10, // how far the mouse can be away to activate an item
+ ignoreZeroValuePoints: false
},
interaction: {
redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow
## -2873,8 +2874,11 ## Licensed under the MIT license.
// use <= to ensure last point takes precedence
// (last generally means on top of)
if (dist < smallestDistance) {
- smallestDistance = dist;
- item = [i, j / ps];
+ jps = j / ps;
+ if(!options.grid.ignoreZeroValuePoints || series[i].data[series[i].datapoints.points.slice(jps * series[i].datapoints.pointsize, (jps + 1) * series[i].datapoints.pointsize)[0]][1] != 0){
+ smallestDistance = dist;
+ item = [i, jps];
+ }
}
}
}

Related

Percentage Change in dc.js/crossfilter

I just started with dc.js and was looking at the NASDAQ example on the main site: https://dc-js.github.io/dc.js/
I created a Fiddle with some sample dummy data and just the two relevant charts for this question.
Similar to the NASDAQ example, I want to have a bubble chart with the Y-Axis being the % Change in value over a timespan controlled by a brush in a different chart. The code for the NASDAQ example does the following:
var yearlyPerformanceGroup = yearlyDimension.group().reduce(
/* callback for when data is added to the current filter results */
function (p, v) {
++p.count;
p.absGain += v.close - v.open;
p.fluctuation += Math.abs(v.close - v.open);
p.sumIndex += (v.open + v.close) / 2;
p.avgIndex = p.sumIndex / p.count;
p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
return p;
},
/* callback for when data is removed from the current filter results */
function (p, v) {
--p.count;
p.absGain -= v.close - v.open;
p.fluctuation -= Math.abs(v.close - v.open);
p.sumIndex -= (v.open + v.close) / 2;
p.avgIndex = p.count ? p.sumIndex / p.count : 0;
p.percentageGain = p.avgIndex ? (p.absGain / p.avgIndex) * 100 : 0;
p.fluctuationPercentage = p.avgIndex ? (p.fluctuation / p.avgIndex) * 100 : 0;
return p;
},
/* initialize p */
function () {
return {
count: 0,
absGain: 0,
fluctuation: 0,
fluctuationPercentage: 0,
sumIndex: 0,
avgIndex: 0,
percentageGain: 0
};
}
);
which I currently interpret as summing(close-open) across all data and dividing by the average of the average daily index. But this is not a percent change formula I am familiar with. (e.g. (new-old)/old x 100)
While it seems to work for the NASDAQ example, my data would be more like the following:
country_id,received_week,product_type,my_quantity,my_revenue,country_other_quantity
3,2017-04-02,1,1,361,93881
1,2017-04-02,4,45,140,93881
2,2017-04-02,4,2,30,93881
3,2017-04-02,3,1,462,93881
2,2017-04-02,3,48,497,93881
etc.. over many months and product_types.
Let's say I was interested in computing the percent change for a particular Country. How do I get the start and end quantities for a given country so I can compute change as end-start/start * 100?
I was thinking of something such as the following (assuming I set up the proper dimensions and everything)
var country_dim = ndx.dimension(function (d) { return d['country_id']; })
var received_day_dim = ndx.dimension(function (d) { return d['received_day']; })
var date_min = received_day_dim.bottom(1)[0]['received_day']
var date_max = received_day_dim.top(1)[0]['received_day']
Then in my custom reduce function currently in the vein of the example (wrong):
var statsByCountry = country_dim.group().reduce(
function (p, v) {
++p.count;
p.units += +v["my_units"];
p.example_rate = +v['my_units']/(v['quantity_unpacked']*90) //place holder for total units per day per country
p.sumRate += p.opp_buy_rate;
p.avgRate = p.opp_buy_rate/p.count;
p.percentageGain = p.avgRate ? (p.opp_buy_rate / p.avgRate) * 100 : 0;
p.dollars += +v["quantity_unpacked"]/2;
// p.max_date = v['received_week'].max();
// p.min_date
//dateDimension.top(Infinity)[dateDimension.top(Infinity).length - 1]['distance'] - dateDimension.top(Infinity)[0]['distance']
return p;
},
function (p, v) {
--p.count;
if (v.region_id > 2) {
p.test -= 100;
}
p.units -= +v["quantity_unpacked"];
p.opp_buy_rate = +v['quantity_unpacked']/(v['quantity_unpacked']*90) //place holder for total units per day per country
p.sumRate -= p.opp_buy_rate;
p.avgRate = p.count ? p.opp_buy_rate/p.count : 0;
p.percentageGain = p.avgRate ? (p.opp_buy_rate / p.avgRate) * 100 : 0;
p.dollars -= +v["quantity_unpacked"]/2;
// p.max_date = v['received_week'].max();
return p;
},
function () {
return {quantity_unpacked: 0,
count: 0,
units: 0,
opp_buy_rate: 0,
sumRate: 0,
avgRate: 0,
percentageGain: 0,
dollars: 0,
test: 0
};//, dollars: 0}
}
);
and my chart:
country_bubble
.width(990)
.height(250)
.margins({top:10, right: 50, bottom: 30, left:80})
.dimension(country_dim)
.group(statsByCountry)
.keyAccessor(function (p) {
return p.value.units;
})
.valueAccessor(function (p) { //y alue
return p.value.percentageGain;
})
.radiusValueAccessor(function (p) { //radius
return p.value.dollars/10000000;
})
.maxBubbleRelativeSize(0.05)
.elasticX(true)
.elasticY(true)
.elasticRadius(true)
.x(d3.scale.linear())
.y(d3.scale.linear())
// .x(d3.scale.linear().domain([0, 1.2*bubble_xmax]))
// .y(d3.scale.linear().domain([0, 10000000]))
.r(d3.scale.linear().domain([0, 10]))
.yAxisPadding('25%')
.xAxisPadding('15%')
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true)
.on('renderlet', function(chart, filter){
chart.svg().select(".chart-body").attr("clip-path",null);
});
Originally thought of having something similar to the following in statsbycountry:
if (v.received_day == date_min) {
p.start_value += v.my_quantity;
}
if (v.received_day == date_max) {
p.end_value += v.my_quantity;
}
This seems a bit clumsy? But if I do this, I don't think this will continually update as other filters change (say time or product)? Ethan suggested I use fake groups, but I'm a bit lost.
With the working fiddle, we can demonstrate one way to do this. I don't really think this is the best way to go about it, but it is the Crossfilter way.
First you need to maintain an ordered array of all data in a group as part of the group using your custom reduce function:
var statsByCountry = country_dim.group().reduce(
function(p, v) {
++p.count;
p.units += +v["my_quantity"];
p.country_rate = p.units / (1.0 * v['country_other_quantity']) //hopefully total sum of my_quantity divided by the fixed country_other_quantity for that week
p.percent_change = 50 //placeholder for now, ideally this would be the change in units over the timespan brush on the bottom chart
p.dollars += +v["my_revenue"];
i = bisect(p.data, v, 0, p.data.length);
p.data.splice(i, 0, v);
return p;
},
function(p, v) {
--p.count;
p.units -= +v["my_quantity"];
p.country_rate = p.units / (1.0 * v['country_other_quantity']) //hopefully total sum of my_quantity divided by the fixed country_other_quantity for that week
p.percent_change = 50 //placeholder for now, ideally this would be the change in units over the timespan brush on the bottom chart
p.dollars -= +v["my_revenue"];
i = bisect(p.data, v, 0, p.data.length);
p.data.splice(i, 1)
return p;
},
function() {
return {
data: [],
count: 0,
units: 0,
country_rate: 0,
dollars: 0,
percent_change: 0
}; //, dollars: 0}
}
);
Above, I've updated your reduce function to maintain this ordered array (ordered by received_week) under the .data property. It uses Crossfilter's bisect function to maintain order efficiently.
Then in your valueAccessor you want to actually calculate your change in value based on this data:
.valueAccessor(function(p) { //y alue
// Calculate change in units/day from first day to last day.
var firstDay = p.value.data[p.value.data.length-1].received_week.toString();
var lastDay = p.value.data[0].received_week.toString();
var firstDayUnits = d3.sum(p.value.data, function(d) { return d.received_week.toString() === firstDay ? d.my_quantity : 0 })
var lastDayUnits = d3.sum(p.value.data, function(d) { return d.received_week.toString() === lastDay ? d.my_quantity : 0 })
return lastDayUnits - firstDayUnits;
})
You do this in the value accessor because it only runs once per filter change, whereas the reduce functions run once per record added or removed, which can be thousands of times per filter.
If you want to calculate % change, you can do this here as well, but the key question for % calculations is always "% of what?" and the answer to that question wasn't clear to me from your question.
It's worth noting that with this approach your group structure is going to get really big as you are storing your entire data set in the groups. If you are having performance problems while filtering, I would still recommend moving away from this approach and towards one based on a fake group.
Working updated fiddle: https://jsfiddle.net/vysbxd1h/1/

How to straighten unneeded turns in a A* graph search result?

I have been working on a JavaScript implementation of the early 90's adventure games and specifically plotting a path from the place the hero is standing to the location the player has clicked on. My approach is to first determine if a strait line (without obstructions) can be drawn, if not then to search for a path of clear way-points using Brian Grinstead's excellent javascript-astar. The problem I'm facing however is the path (while optimal will veer into spaces that would seem to the user an unintended. Here is a classic example of what I'm talking about (the green path is the generated path, the red dots are each turns where the direction of the path changes):
Now I know that A* is only guaranteed to return a path that cannot be simpler (in terms of steps), but I'm struggling to implement a heuristic that weights turns. Here is a picture that show two other paths that would also qualify as just as simple (with an equal number of steps)
The Blue path would present the same number of steps and turns while the red path has the same number of steps and fewer turns. In my code I have a simplifyPath() function that removes steps where the direction changes, so if I could get all possible paths from astar then I could select the one with the least turns, but that's not how A* fundamentally works, so I'm looking for a way to incorporate simplicity into the heuristic.
Here is my current code:
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
getPosition: function(event) {
var bounds = field.getBoundingClientRect(),
x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];
return {
x: x,
y: y,
node: node
}
},
drawObstructions: function() {
context.clearRect (0, 0, 320, 200);
if(img) {
context.drawImage(img, 0, 0);
} else {
context.fillStyle = 'rgb(0, 0, 0)';
context.fillRect(200, 100, 50, 50);
context.fillRect(0, 100, 50, 50);
context.fillRect(100, 100, 50, 50);
context.fillRect(0, 50, 150, 50);
}
},
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
return simplePath;
},
drawPath: function(start, end) {
var path, step, next;
if(this.isPathClear(start, end)) {
this.drawLine(start, end);
} else {
path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
if(path.length > 1) {
step = path[0];
for(next = 1; next < path.length; next++) {
this.drawLine(step, path[next]);
step = path[next];
}
}
}
},
drawLine: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
context.fillStyle = 'rgb(255, 0, 0)';
} else {
context.fillStyle = 'rgb(0, 255, 0)';
}
context.fillRect(x, y, 1, 1);
if(x === end.x && y === end.y) {
break;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
},
isPathClear: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
return false;
}
if(x === end.x && y === end.y) {
return true;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
}
}, graph;
engine.drawObstructions();
graph = (function() {
var x, y, rows = [], cols, js = '[';
for(y = 0; y < 200; y += graphSettings.size) {
cols = [];
for(x = 0; x < 320; x += graphSettings.size) {
cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
}
js += '['+cols+'],\n';
rows.push(cols);
}
js = js.substring(0, js.length - 2);
js += ']';
document.getElementById('Graph').value=js;
return new Graph(rows, { diagonal: true });
})();
return engine;
}, start, end, engine = EngineBuilder(field, 10);
field.addEventListener('click', function(event) {
var position = engine.getPosition(event);
if(!start) {
start = position;
} else {
end = position;
}
if(start && end) {
engine.drawObstructions();
engine.drawPath(start, end);
start = end;
}
}, false);
#field {
border: thin black solid;
width: 98%;
background: #FFFFC7;
}
#Graph {
width: 98%;
height: 300px;
overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>
After digging into Michail Michailidis' excellent answer I've added the following code to my simplifyPath() function) (demo):
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1],
simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}],
i,
finalPath = [simplePath[0]],
classification,
previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
previous = simplePath[0];
for(i = 2; i < simplePath.length; i++) {
if(!this.isPathClear(previous, simplePath[i])) {
finalPath.push(simplePath[i-1]);
previous = simplePath[i-1];
}
}
finalPath.push(end);
return finalPath;
}
Basically after it reduces redundant steps in the same direction, it tries to smooth out the path by looking ahead to see if it can eliminate any steps.
Very very intresting problem! Thanks for this question! So...some observations first:
Not allowing diagonal movement fixes this problem but since you are interested in diagonal movement I had to search more.
I had a look at path simplifying algorithms like:
Ramer Douglas Peuker
(http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm)
and an implementation: https://gist.github.com/rhyolight/2846020.
I added an implementation to your code without success. This algorithm doesn't take into account obstacles so it was difficult to adapt it.
I wonder what would the behavior be (for diagonal movements) if you had used Dijkstra instead of A* or if you used an 'all shortest paths between a pair of nodes' algorithm and then you sorted them by increasing changes in direction.
After reading a bit about A* here http://buildnewgames.com/astar/ I thought that the implementation of A-star you are using is the problem or the heuristics. I tried all the heuristics on the a-star of your code including euclidean that I coded myself and tried also all the heuristics in the http://buildnewgames.com/astar code Unfortunately all of the diagonal allowing heuristics were having the same issue you are describing.
I started working with their code because it is a one-to-one grid and yours was giving me issues drawing. Your simplifyPath that I tried to remove was also causing additional problems. You have to keep in mind that since
you are doing a remapping this could be an issue based on that
On a square grid that allows 4 directions of movement, use Manhattan distance (L1).
On a square grid that allows 8 directions of movement, use Diagonal distance (L∞).
On a square grid that allows any direction of movement, you might or might not want Euclidean distance (L2). If A* is finding paths on the grid but you are allowing movement not on the grid, you may want to consider other representations of the map. (from http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html)
What is my pseudocode algorithm:
var path = A-star();
for each node in path {
check all following nodes till some lookahead limit
if you find two nodes in the same row but not column or in the same column but not row {
var nodesToBeStraightened = push all nodes to be "straightened"
break the loop;
}
skip loop index to the next node after zig-zag
}
if nodesToBeStraightened length is at least 3 AND
nodesToBeStraightened nodes don't form a line AND
the resulting Straight line after simplification doesn't hit an obstruction
var straightenedPath = straighten by getting the first and last elements of nodesToBeStraightened and using their coordinates accordingly
return straightenedPath;
Here is the visual explanation of what is being compared in the algorithm:
Visual Explanation:
How this code will be used with yours (I did most of the changes - I tried my best but there are so many problems like with how you do drawing and because of the rounding of the grid etc - you have to use a grid and keep the scale of the paths accurate - please see also assumptions below):
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
//[...] missing code
removeZigZag: function(currentPath,lookahead){
//for each of the squares on the path - see lookahead more squares and check if it is in the path
for (var i=0; i<currentPath.length; i++){
var toBeStraightened = [];
for (var j=i; j<lookahead+i+1 && j<currentPath.length; j++){
var startIndexToStraighten = i;
var endIndexToStraighten = i+1;
//check if the one from lookahead has the same x xor the same y with one later node in the path
//and they are not on the same line
if(
(currentPath[i].x == currentPath[j].x && currentPath[i].y != currentPath[j].y) ||
(currentPath[i].x == currentPath[j].y && currentPath[i].x != currentPath[j].x)
) {
endIndexToStraighten = j;
//now that we found something between i and j push it to be straightened
for (var k = startIndexToStraighten; k<=endIndexToStraighten; k++){
toBeStraightened.push(currentPath[k]);
}
//skip the loop forward
i = endIndexToStraighten-1;
break;
}
}
if (toBeStraightened.length>=3
&& !this.formsALine(toBeStraightened)
&& !this.lineWillGoThroughObstructions(currentPath[startIndexToStraighten], currentPath[endIndexToStraighten],this.graph?????)
){
//straightening:
this.straightenLine(currentPath, startIndexToStraighten, endIndexToStraighten);
}
}
return currentPath;
},
straightenLine: function(currentPath,fromIndex,toIndex){
for (var l=fromIndex; l<=toIndex; l++){
if (currentPath[fromIndex].x == currentPath[toIndex].x){
currentPath[l].x = currentPath[fromIndex].x;
}
else if (currentPath[fromIndex].y == currentPath[toIndex].y){
currentPath[l].y = currentPath[fromIndex].y;
}
}
},
lineWillGoThroughObstructions: function(point1, point2, graph){
var minX = Math.min(point1.x,point2.x);
var maxX = Math.max(point1.x,point2.x);
var minY = Math.min(point1.y,point2.y);
var maxY = Math.max(point1.y,point2.y);
//same row
if (point1.y == point2.y){
for (var i=minX; i<=maxX && i<graph.length; i++){
if (graph[i][point1.y] == 1){ //obstacle
return true;
}
}
}
//same column
if (point1.x == point2.x){
for (var i=minY; i<=maxY && i<graph[0].length; i++){
if (graph[point1.x][i] == 1){ //obstacle
return true;
}
}
}
return false;
},
formsALine: function(pointsArray){
//only horizontal or vertical
if (!pointsArray || (pointsArray && pointsArray.length<1)){
return false;
}
var firstY = pointsArray[0].y;
var lastY = pointsArray[pointsArray.length-1].y;
var firstX = pointsArray[0].x;
var lastX = pointsArray[pointsArray.length-1].x;
//vertical line
if (firstY == lastY){
for (var i=0; i<pointsArray.length; i++){
if (pointsArray[i].y!=firstY){
return false;
}
}
return true;
}
//horizontal line
else if (firstX == lastX){
for (var i=0; i<pointsArray.length; i++){
if (pointsArray[i].x!=firstX){
return false;
}
}
return true;
}
return false;
}
//[...] missing code
}
//[...] missing code
}
Assumptions and Incompatibilities of the above code:
obstacle is 1 and not 0
the orginal code I have in the demo is using array instead of {x: number, y:number}
in case you use the other a-star implementation, the point1 location is on the column 1 and row 2.
converted to your {x: number, y:number} but haven't checked all the parts:
I couldn't access the graph object to get the obstacles - see ?????
You have to call my removeZigZag with a lookahead e.g 7 (steps away) in the place where you were
doing your path simplification
admittedly their code is not that good compared to the a-star implementation from Stanford but for our purposes it should be irelevant
possibly the code has bugs that I don't know of and could be improved. Also I did my checks only in this specific world configuration
I believe the code has complexity O(N x lookahead)~O(N) where N is the number of steps in the input A* shortest path.
Here is the code in my github repository (you can run the demo)
https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
based on this A* Javascript implementation downloaded from here: http://buildnewgames.com/astar/
Their clickHandling and world boundary code is broken as when you click on the right side of the map the path calculation is not working sometimes. I didn't have time to find their bug. As a result my code has the same issue
Probably it is because the map I put from your question is not square - but anyway my algorithm should be unaffected. You will see this weird behavior is happening if non of my remove ZigZag code runs. (Edit: It was actually because the map was not square - I updated the map to be square for now)
Feel free to play around by uncommenting this line to see the before-after:
result = removeZigZag(result,7);
I have attached 3 before after image sets so the results can be visualized:
(Keep in mind to match start and goal if you try them - direction DOES matter ;) )
Case 1: Before
Case 1: After
Case 2: Before
Case 2: After
Case 3: Before
Case 3: After
Case 4: Before
Case 4: After
Resources:
My code (A* diagonal movement zig zag fix demo): https://github.com/zifnab87/AstarWithDiagonalsFixedZigZag
Original Javascript A* implementation of my demo can be found above (first commit) or here: - http://buildnewgames.com/astar/
A* explanation: http://buildnewgames.com/astar/
A* explanation from Stanford: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
JavaScript A* implementation used by OP's question (Github):
Ramer Douglas Peuker Algorithm (Wikipedia): http://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
Javascript implementation of Douglas Peuker Algorithm: https://gist.github.com/rhyolight/2846020
A* Algorithm (Wikipedia): http://en.wikipedia.org/wiki/A*_search_algorithm
You can use a modified A* algorithm to account for changes in direction. While simplifying the result of the standard A* algorithm may yield good results, it may not be optimal. This modified A* algorithm will return a path of minimal length with the least number of turns.
In the modified A* algorithm, each position corresponds to eight different nodes, each with their own heading. For example, the position (1, 1) corresponds to the eight nodes
(1,1)-up, (1,1)-down, (1,1)-right, (1,1)-left,
(1,1)-up-left, (1,1)-up-right, (1,1)-down-left, and (1,1)-down-right
The heuristic distance from a node to the goal is the the heuristic distance from the corresponding point to the goal. In this case, you probably want to use the following function:
H(point) = max(abs(goal.xcor-point.xcor), abs(goal.ycor-point.ycor))
The nodes that correspond to a particular position are connected to the nodes of the neighboring positions with the proper heading. For example, the nodes corresponding to the position (1,1) are all connected to the following eight nodes
(1,2)-up, (1,0)-down, (2,1)-right, (0,1)-left,
(0,2)-up-left, (2,2)-up-right, (0,0)-down-left, and (2,0)-down-right
The distance between any two connected nodes depends on their heading. If they have the same head, then the distance is 1, otherwise, we have made a turn, so the distance is 1+epsilon. epsilon represents an arbitrarily small value/number.
We know need to have a special case for the both the start and goal. The start and goal are both represented as a single node. At the start, we have no heading, so the distance between the start node and any connected node is 1.
We can now run the standard A* algorithm on the modified graph. We can map the returned path to a path in the original grid, by ignoring the headings. The total length of the returned path will be of the form n+m*epsilon. n is the total length of the corresponding path in the original grid, and m is the number of turns. Because A* returns a path of minimal length, the path in the original grid is a path of minimal length that makes the least turns.
I have come up with somewhat of a fix that is a simple addition to your original code, but it doesn't work in all situations (see image below) because we are limited to what the A* returns us. You can see my jsfiddle here
I added the following code to your simplifyPath function right before the return. What it does is strips out extra steps by seeing if there is a clear path between non-adjacent steps (looking at larger gaps first). It could be optimized, but you should get the gist from what I've got.
do{
shortened = false;
loop:
for(i = 0; i < simplePath.length; i++) {
for(j = (simplePath.length - 1); j > (i + 1); j--) {
if(this.isPathClear(simplePath[i],simplePath[j])) {
simplePath.splice((i + 1),(j - i - 1));
shortened = true;
break loop;
}
}
}
} while(shortened == true);
You can see below that this removes the path that goes in on the left (as in the question) but also that not all the odd turns are removed. This solution only uses the points provided from the A*, not points in between on the path - for example, because the 2nd point does not have a straight unobstructed line to the 4th or 5th points, it cannot optimize point 3 out. It happens a lot less than the original code, but it still does give weird results sometimes.
In edition to nodes having references to their parent nodes. Also store which direction that node came from inside a variable. In my case there was only two possibilities horizontally or vertically. So I created two public static constants for each possibility. And a helper function named "toDirection" which takes two nodes and return which direction should be taken in order to go from one to another:
public class Node {
final static int HORIZONTALLY = 0;
final static int VERTICALLY = 1;
int col, row;
boolean isTravelable;
int fromDirection;
double hCost;
double gCost;
double fCost;
Node parent;
public Node(int col, int row, boolean isTravelable) {
this.col = col;
this.row = row;
this.isTravelable = isTravelable;
}
public static int toDirection(Node from, Node to) {
return (from.col != to.col) ? Node.HORIZONTALLY : Node.VERTICALLY;
}
}
Then you can change your weight calculation function to take turns into account. You can now give a small punishment for turns like:
public double calcGCost(Node current, Node neighbor) {
if(current.fromDirection == Node.toDirection(current, neighbor)) {
return 1;
} else{
return 1.2;
}
}
Full code: https://github.com/tezsezen/AStarAlgorithm
At the risk of potential down voting, I will try my best to suggest an answer. If you weren't using a third party plugin I would suggest a simple pop/push stack object be built however since you are using someone else's plugin it might be best to try and work along side it rather than against it.
That being said I might just do something simple like track my output results and try to logically determine the correct answer. I would make a simple entity type object literal for storage within an array of all possible path's? So the entire object's life span is only to hold position information. Then you could later parse that array of objects looking for the smallest turn count.
Also, since this third party plugin will do most of the work behind the scenes and doesn't seem very accessible to extract, you might need to feed it criteria on your own. For example if its adding more turns then you want, i.e. inside the door looking square, then maybe sending it the coordinates of the start and end arent enouugh. Perhaps its better to stop at each turn and send in the new coordinates to see if a straight line is now possible. If you did this then each turn would have a change to look and see if there is an obstruction stopping a straight line movement.
I feel like this answer is too simple so it must be incorrect but I will try nonetheless...
//Entity Type Object Literal
var pathsFound = function() {
//Path Stats
straightLine: false,
turnCount: 0,
xPos: -1, //Probably should not be instantiated -1 but for now it's fine
yPos: -1,
//Getters
isStraightLine: function() { return this.straightLine; },
getTurnCount: function() { return this.turnCount; },
getXPos: function() { return this.xPos; },
getYPos: function() { return this.yPos; },
//Setters
setStraightLine: function() { this.straightLine = true; },
setCrookedLine: function() { this.straightLine = false; },
setXPos: function(val) { this.xPos = val; },
setYPos: function(val) { this.yPos = val; },
//Class Functionality
incrementTurnCounter: function() { this.turnCount++; },
updateFullPosition: function(xVal, yVal) {
this.xPos = xVal;
this.yPos = yVal.
},
}
This way you could report all the data every step of the way and before you draw to the screen you could iterate through your array of these object literals and find the correct path by the lowest turnCount.
var img,
field = document.getElementById('field'),
EngineBuilder = function(field, size) {
var context = field.getContext("2d"),
graphSettings = { size: size, mid: Math.ceil(size/2)},
engine = {
getPosition: function(event) {
var bounds = field.getBoundingClientRect(),
x = Math.floor(((event.clientX - bounds.left)/field.clientWidth)*field.width),
y = Math.floor(((event.clientY - bounds.top)/field.clientHeight)*field.height),
node = graph.grid[Math.floor(y/graphSettings.size)][Math.floor(x/graphSettings.size)];
return {
x: x,
y: y,
node: node
}
},
drawObstructions: function() {
context.clearRect (0, 0, 320, 200);
if(img) {
context.drawImage(img, 0, 0);
} else {
context.fillStyle = 'rgb(0, 0, 0)';
context.fillRect(200, 100, 50, 50);
context.fillRect(0, 100, 50, 50);
context.fillRect(100, 100, 50, 50);
context.fillRect(0, 50, 150, 50);
}
},
simplifyPath: function(start, complexPath, end) {
var previous = complexPath[1], simplePath = [start, {x:(previous.y*graphSettings.size)+graphSettings.mid, y:(previous.x*graphSettings.size)+graphSettings.mid}], i, classification, previousClassification;
for(i = 1; i < (complexPath.length - 1); i++) {
classification = (complexPath[i].x-previous.x).toString()+':'+(complexPath[i].y-previous.y).toString();
if(classification !== previousClassification) {
simplePath.push({x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid});
} else {
simplePath[simplePath.length-1]={x:(complexPath[i].y*graphSettings.size)+graphSettings.mid, y:(complexPath[i].x*graphSettings.size)+graphSettings.mid};
}
previous = complexPath[i];
previousClassification = classification;
}
simplePath.push(end);
return simplePath;
},
drawPath: function(start, end) {
var path, step, next;
if(this.isPathClear(start, end)) {
this.drawLine(start, end);
} else {
path = this.simplifyPath(start, astar.search(graph, start.node, end.node), end);
if(path.length > 1) {
step = path[0];
for(next = 1; next < path.length; next++) {
this.drawLine(step, path[next]);
step = path[next];
}
}
}
},
drawLine: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
context.fillStyle = 'rgb(255, 0, 0)';
} else {
context.fillStyle = 'rgb(0, 255, 0)';
}
context.fillRect(x, y, 1, 1);
if(x === end.x && y === end.y) {
break;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
},
isPathClear: function(start, end) {
var x = start.x,
y = start.y,
dx = Math.abs(end.x - start.x),
sx = start.x<end.x ? 1 : -1,
dy = -1 * Math.abs(end.y - start.y),
sy = start.y<end.y ? 1 : -1,
err = dx+dy,
e2, pixel;
for(;;) {
pixel = context.getImageData(x, y, 1, 1).data[3];
if(pixel === 255) {
return false;
}
if(x === end.x && y === end.y) {
return true;
} else {
e2 = 2 * err;
if(e2 >= dy) {
err += dy;
x += sx;
}
if(e2 <= dx) {
err += dx;
y += sy;
}
}
}
}
}, graph;
engine.drawObstructions();
graph = (function() {
var x, y, rows = [], cols, js = '[';
for(y = 0; y < 200; y += graphSettings.size) {
cols = [];
for(x = 0; x < 320; x += graphSettings.size) {
cols.push(context.getImageData(x+graphSettings.mid, y+graphSettings.mid, 1, 1).data[3] === 255 ? 0 : 1);
}
js += '['+cols+'],\n';
rows.push(cols);
}
js = js.substring(0, js.length - 2);
js += ']';
document.getElementById('Graph').value=js;
return new Graph(rows, { diagonal: true });
})();
return engine;
}, start, end, engine = EngineBuilder(field, 10);
field.addEventListener('click', function(event) {
var position = engine.getPosition(event);
if(!start) {
start = position;
} else {
end = position;
}
if(start && end) {
engine.drawObstructions();
engine.drawPath(start, end);
start = end;
}
}, false);
#field {
border: thin black solid;
width: 98%;
background: #FFFFC7;
}
#Graph {
width: 98%;
height: 300px;
overflow-y: scroll;
}
<script src="http://jason.sperske.com/adventure/astar.js"></script>
<code>Click on any 2 points on white spaces and a path will be drawn</code>
<canvas id='field' height
='200' width='320'></canvas>
<textarea id='Graph' wrap='off'></textarea>

Dc cross-filter not working

Building a grails application using dc & cross-filter js libraries here, but facing an extremely weird issue in the visualization somehow.
My visualization is of 5 bar charts, which are interconnected to each other using dc and cross-filter js libraries.
So, there are simple metrics, calculated metrics (%) and lastly calculated metrics (without %). For these three types, there are three different types of if-else in each part of code (for 5 bar charts)
But, the problem lies in the last if-else, here the cross-filter goes wrong and we get negative values after a couple of selections and all the bar charts are lifted off the x-axis. This is extremely weird and I don't understand what is going wrong here. (See image below for reference)
We have the same code snippet in the second if-else and the third if-else, but the third if-else screws up the functionality of cross-filter. Can someone please explain what is going wrong here?
Code :
var devValue = facts.dimension(function (d) {return d.c;});
var a = ($('metric').value);
// Basic metrics
if(a == "Product views"||a == "Visits"||a == "Units")
{
var devValueGroupSum = devValue.group().reduceSum(function(d) { return +d.g;});
barChart4.width(600)
.height(250)
.margins({top: 10, right: 100, bottom: 20, left: 80})
.dimension(devValue)
.yAxisLabel($('metric').value)
.group(devValueGroupSum)
.transitionDuration(800)
.centerBar(true)
.gap(60)
.x(d3.scale.ordinal().domain(["DESKTOP/LAPTOP", "SMARTPHONES", "TABLETS","OTHERS"]))
.brushOn(false)
.title(function(d) { return d.key + ": " + d3.round(d.value,2); })
.elasticY(true)
.barPadding(0.5)
.xUnits(dc.units.ordinal);
}//end of if
// Calculated metrics (%)
else if(a == "Conversion Rate"||a=="Bounce Rate")
{
var devValueGroupSum = devValue.group().reduce(
function (p, v) {
p.sumIndex1 += v.g
p.sumIndex2 += v.h
if (p.sumIndex2 === 0)
p.avgIndex = 0;
else
p.avgIndex = (p.sumIndex1 / p.sumIndex2) * 100 ;
return p;
},
function (p, v) {
p.sumIndex1 -= v.g;
p.sumIndex2 -= v.g;
return p;
},
function () {
return {sumIndex1: 0,sumIndex2:0, avgIndex: 0};
}
);//end of reduce
barChart4.width(600)
.height(250)
.margins({top: 10, right: 100, bottom: 20, left: 80})
.dimension(devValue)
.group(devValueGroupSum)
.valueAccessor(function (p) {
return p.value.avgIndex;
})
.transitionDuration(800)
.yAxisLabel($('metric').value)
.centerBar(true)
.gap(60)
.x(d3.scale.ordinal().domain(["DESKTOP/LAPTOP", "SMARTPHONES", "TABLETS","OTHERS"]))
.brushOn(false)
.title(function(d) { return d.key + ": " + d3.round(d.value.avgIndex,2); })
.elasticY(true)
.barPadding(0.5)
.xUnits(dc.units.ordinal);
}//end of else-if
// Calculated metrics ( without %)
else if(a == "Average Order Size(AOS)" || a=="Average Unit Revenue(AUR)" || a=="Units per order")
{
var devValueGroupSum = devValue.group().reduce(
function (p, v) {
p.sumIndex1 += v.g
p.sumIndex2 += v.h
if (p.sumIndex2 === 0)
p.avgIndex = 0;
else
p.avgIndex = (p.sumIndex1 / p.sumIndex2) * 1 ;
return p;
},
function (p, v) {
p.sumIndex1 -= v.g;
p.sumIndex2 -= v.g;
return p;
},
function () {
return {sumIndex1: 0,sumIndex2:0, avgIndex: 0};
}
);//end of reduce
barChart4.width(600)
.height(250)
.margins({top: 10, right: 100, bottom: 20, left: 80})
.dimension(devValue)
.group(devValueGroupSum)
.valueAccessor(function (p) {
return p.value.avgIndex;
})
.transitionDuration(800)
.yAxisLabel($('metric').value)
.centerBar(true)
.gap(60)
.x(d3.scale.ordinal().domain(["DESKTOP/LAPTOP", "SMARTPHONES", "TABLETS","OTHERS"]))
.brushOn(false)
.title(function(d) { return d.key + ": " + d3.round(d.value.avgIndex,2); })
.elasticY(true)
.barPadding(0.5)
.xUnits(dc.units.ordinal);
}
else
{
}//end of else
UPDATE:
okay, with the help of answers, I changed my code as following, the negative bars have disappeared but still only the initial view is correct.
After which, if I select any of the bars to filter across all the charts, the filtering does not happen. Charts don't change anymore.
Code #2 :
function (p, v) {
//snippet begins
p.sumIndex1 += v.g
p.sumIndex2 += v.h
if (p.sumIndex2 === 0)
p.avgIndex = 0;
else
p.avgIndex = (p.sumIndex1 / p.sumIndex2) ;
//snippet ends
p.sumIndex1 -= v.g;
p.sumIndex2 -= v.h;
return p;
},
I have tried adding the snippet before and after the removal of records from the callback method, but neither of them work
All approaches/suggestions are most welcome
Hard to say without a working example, but I think your problem is that in your add function you are adding "p.sumIndex2 += v.h" while in your remove function you are subtracting "p.sumIndex2 -= v.g". So your sumIndex2 isn't really tracking any specific value. You should add and subtract the same thing from it so that adding a record and then removing it results in no change.
Additionally, you should recalculate your average value when you remove records, not just when you add them. Your average is going to be wrong after filters are applied.
I'd also recommend just creating all three groups you need and the bar chart with the default group. Then switch out the group on the bar chart and rerender it when the group you want to display changes.

Canvas paint bucket/flood fill tool gets stuck in loop

I'm using the code from this tutorial. I'm also letting the user select the color using <input type="color"> (and I convert that to RGB since it returns a hex value). The fill seems to work just fine if the color is left at the default (set to black on initialization). If the color is changed using the input though, sometimes it ends up freezing. From what I can tell it gets stuck in this loop:
while (pixelStack.length) {...}
There is a section in the loop where it pushes new values to the pixelStack array:
while (y++ < height - 1 && matchStartColor(pixelPos)) {
colorPixel(pixelPos);
if (x > 0) {
if (matchStartColor(pixelPos - 4)) {
if (!reachLeft) {
pixelStack.push([x - 1, y]);
reachLeft = true;
}
} else if (reachLeft) {
reachLeft = false;
}
}
if (x < width - 1) {
if (matchStartColor(pixelPos + 4)) {
if (!reachRight) {
pixelStack.push([x + 1, y]);
reachRight = true;
}
} else if (reachRight) {
reachRight = false;
}
}
pixelPos += width * 4;
}
As I understand this, the only reason it would constantly push to the pixelStack array is if the matchStartColor function never came back false.
function matchStartColor(pixelPos) {
var r = imageData.data[pixelPos];
var g = imageData.data[pixelPos + 1];
var b = imageData.data[pixelPos + 2];
return (r && g && b);
}
But I don't see why this sometimes works and other times not.
Basically the pixelStack array ends up growing continually so it can never exit the loop. I made a jsFiddle with a working example (though it will lock up eventually if you change the color and try to fill). It doesn't seem to fail 100% of the time when the color is changed, but change the color enough and fill and it eventually does (and some colors seem to fail more than others. The pinkish/red in the top left of the color picker window is a bad one for example).
It seems your issue is in your implementation of matchStartColor.
With the following implementation:
function matchStartColor(pixelPos) {
var r = imageData.data[pixelPos];
var g = imageData.data[pixelPos + 1];
var b = imageData.data[pixelPos + 2];
return (r && g && b);
}
This will only ever return false if either r, g or b are 0. When you use black (or any color with a 0 component), this works. If you pick a color where none of the components are 0, this function never returns false.
I think what you actually want to do here is compare the color of the image to the color that you've chosen.
Based on how you use this function, I think this should work:
function matchStartColor(pixelPos) {
var r = imageData.data[pixelPos];
var g = imageData.data[pixelPos + 1];
var b = imageData.data[pixelPos + 2];
return (r !== curColor.r || g !== curColor.g || b !== curColor.b);
}
https://jsfiddle.net/6Uy3U/4/

How to add a new tick to the autogenerated ticks in flot

I want to add just a tick to the existing autogenerated ticks, how can I do it ?
This is the fiddle. I want to add tick 100 to the existing generated tick series. Can such a hack be done in flot ?
I was hoping this would be possible using the hooks functionality in flot but unfortunatly the ticks are generated and then the labels are added without any hooks in between.
So...
Your best bet would be to steal flots automatic tick generator, modify it to do what you want and then add it as the tick function in your options.
customTickGen = function (axis) {
/* BEGIN STOLEN FLOT METHOD */
var ticks = [],
start = axis.tickSize * Math.floor(axis.min / axis.tickSize),
i = 0,
v = Number.NaN,
prev;
do {
prev = v;
v = start + i * axis.tickSize;
ticks.push(v);
++i;
} while (v < axis.max && v != prev);
/* END OF STOLEN FLOT CODE */
/* Now find the spot and put a 100 in */
for (var i = 0; i < ticks.length - 1; i++){
if (ticks[i] < 100 && ticks[i+1] > 100){
ticks.splice(i,0,100);
break;
}
}
return ticks;
}
Here's a working fiddle.

Categories

Resources