[possible bug]Values in array as attribute of object set to undefined - javascript

I have encountered one of the most bizzare and frustrating behaviours yet. I have sample data:
var nodes = [ //Sample data
{
ID: 1,
Chart: 1,
x: 50,
y: 50,
width: 100,
height: 80,
color: "#167ee5",
text: "Start",
label: "Start",
targets: [2]
},
{
ID: 2,
Chart: 1,
x: 500,
y: 170,
width: 100,
height: 80,
color: "#167ee5",
text: "End",
label: "End",
targets: [3]
},
{
ID: 3,
Chart: 1,
x: 270,
y: 350,
width: 100,
height: 80,
color: "#167ee5",
text: "Mid",
label: "Mid",
targets: []
}
];
for my web application. The issue is with the targets attribute. As you can see it is array. However when I do
console.log(nodes[0]);
and inspect the result in the browser it shows that the value for targets at index 0 is undefined. Same for every other targets that has some values in them (whether 1 or more).
However if I do
console.log(nodes.[0].targets);
it prints out [2]. If I do Array.isArray(nodes[0].targets) it returns false, yet if I do console.log(nodes[0]) and inspect the result in the browser console, it shows that the object prototype is in fact Array and simply the value at index 0 is undefined.
It worked the day before and now it doesn't. The only thing I did was I restructured the object that uses this variable later. But the console log is being called before the object is even instantiated for the first time (and it doesn't change the nodes var anyway, only reads it).
Does anyone have any clues as to what might be causing this behaviour. If it helps I am using Paperscript and this code runs in the paperscript scope (as it did before when everything worked fine).
UPDATE
Ok after more blind debugging I have determined the block of code that causes the issue, how or why is completely beyond me.
Basically I define an object constructor beflow. The constructor loops through the nodes, makes Paperscript shapes and adds the targets to the arbitrary data attribute of the paperJS path object:
function Flowchart(nodes, chartdata) {
//Member vars. They are only used internally
var connections = [];
var shapes = [];
var connectors = [];
//Constructor operations
createShapes(nodes); //If I uncomment this, the problem goes away
//...
function createShapes(nodes) {
nodes.forEach(function (node) { //for each node data entry
console.log(node); //At this point, the targets are screwed up already
var point = new Point(node.x, node.y); //make a PaperJS point for placement
var size = new Size(node.width, node.height); //make a PaperJS size object
var shape = makeRectangle(point, size, 8, node.color); //Pass to the object instantiating function
shape.data = { //Store arbitrary data for programming reference.
ID: node.ID,
label: node.label,
text: node.text,
'connectors': {
to: [],
from: []
},
targets: node.targets //this is undefined
};
console.log(node.targets) //this logs [2] or [3] but not Array[1]...
shapes.push(shape); //Store reference for later
});
shapes.forEach(function (shape) { //loop though all drawn objects
if (shape.data.targets.length > 0) { //if shape has targets
var targets = _.filter(this.shapes, function (target) {
return _.contains(shape.data.targets, target.data.ID);
});
for (var i = 0; i < shape.data.targets.length; i++) {
shape.data.targets[i] = targets[i]; //Replace the ID-type reference with drawn object reference
}
}
});
}
//... The rest of the object
}
console.log(nodes);
//It doesnt seem to matter whether i put this before or after instantiating.
//It doesnt even matter IF I instantiate in the first place.
var chart = new Flowchart(nodes, chartdata);

This behaviour has been caused by the changes to how Chrome treats enumerable properties of objects. Because Chrome updates silently, it's impossible to notice.
It must have been causing me a lot of headache if I remembered the cause after all this time... (Also it's embarrassing how bad I was at writing questions, but I guess that I realise it means I have progressed since then somewhat).

Related

Replace an object in FabricJS?

To implement a undo/redo functionality, I have constructed an array "history" that gets filled with the latest changes based on canvas.on() events.
console.log dump:
History:
(3) […]
​
0: Object { target: {…} } //first object added
​
1: Object { e: mouseup, target: {…}, transform: {…}, … } //first object modified
​
2: Object { target: {…} } //another object added
​
length: 3
To walk back the stack of changes, I wanted to use history[step].target which contains the modified object at this stage (step).
I now look for a method to overwrite an object in the object array of fabric.
The function canvas.item(0) gives the object at position 0 on the current canvas but how can I overwrite that object with a different object (from the history[].target array)?
Note: Solutions I found for a undo/redo are seemingly based on serializing the whole canvas into JSON and saving this into an array of JSONs. I didn't want to do this since it seems a bit unsophisticated to always serialize/unserialize the whole canvas with x objects in it just to undo a small change and the history contains many usefull informations about what was changed and on which object ect.
Note2: Of course I probably could just canvas.remove(canvas.item(0)) and then canvas.add(history[x].target), however this unfortunately messes up my object stack when there's more than one objects, as canvas.item(1) becomes canvas.item(0) and the reverted change now becomes canvas.item(1) (or 2, 3... ect depending on how many items on the canvas). When I then have like a list of objects displayed depending on their position in the object stack, this get's rearranged and could confuse the user.
This is a simple example.
Given 2 rect on on a canvas, a function will add one back in the middle of those 2, and another will replace the one at position 0.
canvas#insertAt does work as suggested from #gabriele-petroli
var c = new fabric.Canvas('c');
c.add(new fabric.Rect({ fill: 'blue', width: 100, height: 100 }));
c.add(new fabric.Rect({ fill: 'green', left: 150, width: 100, height: 100 }));
var redRect = new fabric.Rect({ fill: 'red', left: 75, width: 100, height: 100 });
var purpleRect = new fabric.Rect({ fill: 'purple', left: 0,
width: 100, height: 100 });
setTimeout(function() {
c.insertAt(purpleRect, 0, true);
c.insertAt(redRect, 1);
}, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.5.0/fabric.min.js"></script>
<canvas id="c" width="500" height="500" ></canvas>
As far as I know it is not possible to overwrite an object.
But this might help for your Note2:
In addition to Gabriele Petriolis comment, you can position the object in the stack after inserting:
Canvas.moveTo()
Canvas.bringToFront()
Canvas.bringForward()
Canvas.sendBackwards()
Canvas.sendToBack()
See: http://fabricjs.com/docs/fabric.Canvas.html

vis.js - How to resize nodes at runtime

I would like to resize nodes of a vis.js network at runtime. My goal is to create a slidercontrol to expand all nodes (an labels) or collapse them.
First, I've tried to manipulate the Scaling values.
var options = {nodes: {scaling: {label: {max: 180 , maxVisible: 180}}}};
network.setOptions(options);
But there are no results.
My second idea was to manipulate the value of every single node.
function IncNodeSize(Increment) {
var CurrentNodes = nodesDS.get();
for (var i = 0; i < CurrentNodes.length; i++) {
CurrentNodes[i].value = CurrentNodes[i].value + 100;
}
}
nodesDS.update(CurrentNodes);
}
But this didn't work either.
Has anyone an idea how to resize nodes at runtime?
With the help of the Developer Community I have solved the issue.
At first I had to change the options and than I had to update the nodes (with no datachange)
This two things together changed the sizes.
In my case I simply wanted to resize a single node. I too tried the DataSet.update() method, but to no avail. What did work, however, was to simply set the new property directly on the node object and call Network.setData(). Here's an example:
const nodes = new vis.DataSet([
{ id: 1, label: 'Node 1', shape: 'circle', margin: 20 },
{ id: 2, label: 'Node 2', shape: 'circle', margin: 20 },
]);
const edges = new vis.DataSet([
{ from: 1, to: 2 }
]);
const dta = { nodes: nodes, edges: edges };
const network = new vis.Network(
document.getElementById('graph'),
dta,
{ interaction: { zoomSpeed: 0.2 } }
);
setTimeout(function() {
const node1 = nodes.get(1);
node1.margin = 50;
network.setData(dta);
}, 2000);
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<div id="graph" style="width: 100%; height: 100%"></div>
Notice that I make sure to have a reference to the object (the dta constant) that includes both the nodes DataSet and the edges DataSet, before I start manipulating the node. That way I can call Network.setData() using that dta object.

Multiple shape fadout with Kinetic js produces a stack overflow

Hello Stackoverflow community
As I am trying to build a small game, i came to a problem.
Somehow, when i try to fade out multiple shapes, respectivly a group with the shapes in it, some of the shapes will not fade out or browser gets a stack overflow.
So as i tried out several hours to fix the problem i need your help.
Heres the link to a little fiddle i made: http://jsfiddle.net/hnBPT/
As you can see theres a function newFadeShapesOut() which needs the nodes that should be fade out and also the layer of the nodes.
It moves the nodes into a group and fades the group out. Somehow and sometimes, one or more shapes will not fade out or there occurs a fatal error.
Function for fadeout:
function newFadeShapesOut(shapes, layer, callback, speed){
if(typeof(speed) == 'undefined'){
speed = 1;
}
var g = new Kinetic.Group();
console.log(layer.getChildren().length);
console.log(shapes.length);
layer.add(g);
shapes.each(function(shape){
shape.moveTo(g);
});
console.log(layer.getChildren().length);
console.log(shapes.length);
var tween = new Kinetic.Tween({
node: g,
opacity: 0,
duration: speed,
onFinish: function(){
if(typeof(callback) != 'undefined'){
callback();
tween.destroy();
}
}
}).play();
}
PS: Google Chrome is recommend, firefox tends to crash.
Thank you for your help.
EDIT: Sorry i forgot about that, you can activate the script by clicking the red square.
There's some strange behaviour going on here. Look at my comments as I tried to rewrite your function:
function fadeShapesOut(layer, callback, speed) {
var children = layer.children;
//The layer here already shows some children have moved.
//2 children remain, 1 text and 1 rect.
console.log("LAYER");
console.log(layer);
//Again, children shows that there are only 2 children of layer at this point: Test 2 and Button Rect
console.log('CHILDREN');
console.log(children);
if(typeof(speed) == 'undefined'){
speed = 1;
}
var group = new Kinetic.Group();
layer.add(group);
children.each(function(child) {
console.log("CHILD");
console.log(child); //This spits out Test 1, Test 3 and the newly added Group. (Strange order???
child.moveTo(group);
});
//Since group is already added to the layer, you're all of layer's children to group, including group itself. Which is causing a never ending loop of references to group including itself - causing the stack overflow.
var tween = new Kinetic.Tween({
node: group,
opacity: 0,
duration: speed,
onFinish: function(){
if(typeof(callback) != 'undefined'){
callback();
tween.destroy();
}
}
}).play();
}
What's messing you up is that the group is being considered a child of layer (Even though it hasn't been added yet in the order of function calls, which is strange behaviour to me). So when you loop through the children of layer in the each function, you're trying to move group --> group which screws up the reference in a never ending loop.
I logged a bunch of things in my fiddle, so go ahead and take a look to see some of the strange behaviour I was talking about above.
Anyways, if your callback is going to destroy the layer, what is the point of moving everything to a new group in the function? That Group is messing your code up and I don't see the point of it if you're just going to destroy the layer.
Instead you can achieve the effect you want by just tweening the layer itself:
function fadeLayer(layer, callback, speed) {
var tween = new Kinetic.Tween({
node: layer,
opacity: 0,
duration: 2,
onFinish: function(){
layer.destroy();
tween.destroy();
}
}).play();
}
If you must stick with your original function format, then you can grab children by using names:
newsobj[n] = new Kinetic.Text({
nid: n,
x: 140,
y: ((n == 0) ? 294.5 : 304.5 ),
text: news[n],
fill: 'white',
fontFamily: 'Corbel W01 Regular',
fontSize: 11.26,
name: 'fadeThisAway'
});
button = new Kinetic.Rect({
x: 10,
y: 10,
width: 100,
height: 100,
fill: 'red',
name: 'fadeThisAway'
});
In my example, I used the name fadeThisAway. And then, using your old function:
function newFadeShapesOut(layer, callback, speed){
var shapes = layer.get('.fadeThisAway');
if(typeof(speed) == 'undefined'){
speed = 1;
}
var g = new Kinetic.Group();
console.log(layer.getChildren().length);
console.log(shapes.length);
layer.add(g);
shapes.each(function(shape){
shape.moveTo(g);
});
console.log(layer.getChildren().length);
console.log(shapes.length);
var tween = new Kinetic.Tween({
node: g,
opacity: 0,
duration: speed,
onFinish: function(){
if(typeof(callback) != 'undefined'){
callback();
tween.destroy();
}
}
}).play();
}
Instead of passing shapes through the function, just call
var shapes = layer.get('.fadeThisAway');
at the beginning of the function (you're passing layer through the function already anyways) to grab the children that are named fadeThisAway. (Note: This works because the group is not named fadeThisAway)
Working example and comments inside: JSFIDDLE
UPDATE
Okay so I made a basic example of the issue with layer.children
2nd JSFIDDLE
And it looks like that's just how the children of layer works. This proves that you definitely have to distinguish between shapes and group, because the group will always be considered a child of layer.
The naming method works to distinguish your shapes between layers by giving all shapes a common name that excludes groups.
After several attempts to bend projeqht's function to my way i finally did it!
Somehow, the collection shapes just updates itself when adding the group to the layer!
If i use an array instead, it works.
Hope it helps someone!
So here my solution which works like a charm.
function fadeShapesOut(shapes, callback, speed){
layer = shapes[0].getLayer();
if(typeof(speed) == 'undefined'){
speed = 1;
}
var g = new Kinetic.Group();
layer.add(g);
for(i in shapes){
shapes[i].moveTo(g);
}
var tween = new Kinetic.Tween({
node: g,
opacity: 0,
duration: speed,
onFinish: function(){
if(typeof(callback) != 'undefined'){
callback();
}
tween.destroy();
}
}).play();
}
If you have further questions, don't mind contacting me.

Highcharts: passing additional information to a tooltip

I have an array of data points that I am passing to a Highcharts chart that looks like
mydata = [{
x: 1,
y: 3,
nameList: ["name1", "name2"]
}, {
x: 2,
y: 4,
nameList: ["name3", "name4"]
}]
I build the chart like this:
$("#chart").highcharts("StockChart", {
series: [{
data: mydata
}, {
data: yourdata
}]
});
Now, I would like to be able to access the nameList array from the shared tooltip, which I'm trying to do as follows:
tooltip: {
formatter: function() {
var s = "";
$.each(this.points, function(i, point) {
s += point.point.nameList;
});
return s;
},
shared: true
}
but when examining the point objects in Firebug using console.log(point), I can't seem to find the nameList entry anywhere in them. How could I access this auxiliary information in a shared series tooltip? All help is appreciated.
Eureka!
By default, Highcharts will accept several different types of input for the data of a series, including
An array of numerical values. In this case, the numberical values will be interpreted
and y values, and x values will be automatically calculated, either starting at 0 and
incrementing by 1, or from pointStart and pointInterval given in the plotOptions.
An array of arrays with two values. In this case, the first value is the x value and the
second is the y value. If the first value is a string, it is applied as the name of the
point, and the x value is incremented following the above rules.
An array of objects with named values. In this case the objects are point configuration
objects as seen below.
However, the treatment of type 3 is different from types 1 and 2: if the array is greater than the turboThreshold setting, then arrays of type 3 won't be rendered. Hence, to fix my problem, I just needed to raise the turboThreshold setting like so:
...
plotOptions: {
line: {
turboThreshold: longestArray.length + 1
}
},
...
and the chart renders the longestArray data properly. Hurray! The only drawback is that there is a considerable time spent rendering the data for much longer arrays due to "expensive data checking and indexing in long series." If any of you know how I might be able to bypass this checking or otherwise be able to speed up the processing of this data, I'd be extremely thankful if you'd let me know how.
I can see it here:
tooltip: {
formatter: function() {
var s = "";
console.log(this.points[0].point.nameList); // ["name1", "name2"]
$.each(this.points, function(i, point) {
s += point.point.nameList;
});
return s;
},
shared: true
}

jQuery each going outside of function

I have a jqPlot chart that I want to add links on and I believe I figured out a way to do it using an array such as [[[1,2,"http://google.com"]],[[2,3,"http://yahoo.com]]] however, when I try to load this via XML, jQuery, and Ajax it doesn't quite work.
I believe that the problem lies within the .each clauses found in this code:
function getBars(xml)
{
var categoryid = 1;
var bars = [];
$(xml).find("category").each(
function()
{
bars.push(loadBars(categoryid,$(this)));
categoryid++;
});
return bars;
}
function loadBars(categoryid,xml)
{
var bar = [];
var bars = [];
$(xml).find("bar").each(function()
{
bar.push(parseInt(categoryid));
bar.push(parseInt($(this).attr("size")));
bar.push($(this).attr("link"));
bars.push(bar);
});
$("#debug").append("\nBAR:")
debug2dArray(bars);
return bars;
}
The XML looks like:
<?xml version="1.0"?>
<chart>
<category>
<bar size="20" link="http://google.com"/>
</category>
<category>
<bar size="70" link="http://yahoo.com" />
</category>
</chart>
Here is a jsFiddle
Update
After updating the variables to be non-global, the chart now displays right, but two of the same values are still being added to the array. Code has been updated to reflect changes.
I haven't digested your whole code yet, but one really fatal pitfall you're doing is using variables in your functions that haven't been declared with var (I'm particularly looking at how you've used your bar variable on both functions).
When you use a variable without declaring it with var like you're doing here, you're bringing the variable to a global visibility. That means that that variable is the same variable used (most) everywhere in your code. The same bar in the first function is the same bar in the second.
When your two functions start, the first thing it does is clear the bar variable (i.e. bar = [];). Since they're sharing bar references, calling one function effectively nullifies what the other did.
Is this your intention? If not (or even so), you should declare your variable with var:
var categoryId = 1,
bar = [];
In addition to the lack of var, you are returning variables at the end of the each iterators, instead of the end of the function. Here's a working fiddle: http://jsfiddle.net/fwRSH/1/
function loadBars(categoryid, xml) {
var bar = [];
var bars = [];
$(xml).find("bar").each(function() {
bar.push(parseInt(categoryid, 10));
bar.push(parseInt($(this).attr("size"), 10));
bar.push($(this).attr("link"));
bars.push(bar);
//$("#debug").append("\nBAR:"); //not defined in fiddle, commented out
//debug2dArray(bars); //not defined in fiddle, commented out
});
return bars; //moved from end of "each" iterator to here.
}
function getBars(xml) {
var categoryid = 1;
var bars = [];
$(xml).find("category").each(function() {
bars.push(loadBars(categoryid, $(this)));
categoryid++;
});
return bars;
}
$(document).ready(function() {
var bars = [];
$("div#barchart").css("background-color", "#F00");
$("div#barchart").css("height", "200px");
$("div#barhcart").css("width", "400px");
//moved for debugging
bars = getBars($('div#xmlDI'));
/* returns:
* [
* [
* [1, 20, "http://google.com"]
* ],
* [
* [2, 70, "http://yahoo.com"]
* ]
* ]
*/
$.jqplot("barchart", bars, {
seriesDefaults: {
renderer: $.jqplot.BarRenderer,
rendererOptions: {
fillToZero: true
}
},
axes: {
// Use a category axis on the x axis and use our custom ticks.
xaxis: {
renderer: $.jqplot.CategoryAxisRenderer,
ticks: ['one', 'two'],
autoscale: true
},
yaxis: {
autoscale: true
}
}
});
});​
None of your variables are declared using var, particularly the bars array. This causes them to be implicitly global, and you overwrite the variable every time you call loadBars.
I am not sure how you want your graph to look like. Because the data you provide to the graph, in general terms, is 'correctly' displayed. If you would write it in the following way:
[
[[1, 30, "http://google.com"], [2,0,""]],
[[1,0,""],[2, 40, "http://yahoo.com"]]
]
...it would give exactly the same results, the library just assumes that the data which is not provided for a particular series is 0 and this is how it is treated as visible here.
Since you do not like it this way my guess is that you made a formatting error in your data variable, as we can see here the 'gap' is gone.
Therefore, I think that the below is the format you are after:
[[
[1, 30, "http://google.com"],
[2, 40, "http://yahoo.com"]
]]
Additionally, as it goes to clicking on a bar of a bar chart you could find useful the answer to the problem. There you could see how to capture the click and how to open a URL. You would just need to slightly adopt it to your need as I used a global array of URLs.
Code to parse the XML:
var bars = [], cat = 0;
$.ajax({
type: 'GET',
url: 'plotlinks.xml',
dataType: "xml",
cache: true,
success: function(data, textStatus, jqXHR) {
$(data).find("category").each( function() {
var barSet = [cat];
$(this).find("bar").each(function() {
var $elt = $(this);
barSet.push([$elt.attr('size'),$elt.attr('link')]);
});
cat++;
bars.push(barSet);
});
// bars is an array; each element is an array.
// The first element in the inner array is the
// category "index" (0,1,2,...). All other
// elements represent a link for that category.
// Those elements are arrays of [size,url].
alert($.stringifyJSON(bars));
}
});
Resulting json:
[[0,
["20","http://google.com"]
],
[1,
["70","http://yahoo.com"]
]
]

Categories

Resources