2

I'm creating a JS chart library and I'm struggling with pie/doughnut charts when I change the data. My visuals are created with SVG elements, and creating a new chart is fine. Where I'm struggling is changing the data on an existing chart. Once the chart has been drawn, I don't want the chart to "open up" when sectors are added or removed.

So for example, if my original data (say arrOld) is

arrOld = [
    {name: "bacon", amount: 50},
    {name: "eggs", amount: 25},
    {name: "sausages", amount: 15},
    {name: "toast", amount: 10}
];

and I want to animate a chart into a new data array (say, arrNew) which is

arrNew = [
    {name: "bacon", amount: 50},
    {name: "hash browns", amount: 8},
    {name: "eggs", amount: 25},
    {name: "toast", amount: 10},
    {name: "sausages", amount: 15}
];

I want the new member "hash browns" to "grow" from 0° between "bacon" and "eggs", forcing them apart. That part I've pretty much implemented successfully. Because "toast" and "sausages" have swapped places, I want one of them animated (squashed) down to 0° (and then get removed) whilst a new replacement sector "grows" in its new position. So I will effectively have an intermediate array (say, arrTemp), which will look something like:

arrTemp = [
    {name: "bacon", amount: 50},
    {name: "hash browns", amount: 8, new: true},
    {name: "eggs", amount: 25},
    {name: "sausages", amount: 15, removeMe:true}
    {name: "toast", amount: 10},
    {name: "sausages", amount: 15, new:true}
];

The data I am actually dealing with is way more complex than this example, but this shows the basic idea. I can remove the temporary array after the animation is complete, and the removeMe and new properties are necessary for the animation, but don't need to remain after the animation has finished.

It is essential that the temporary array contains the members in the same order as the original array (even though they may have the "removeMe" property set to true) because that is the starting point of the transition animation.

I've written 5 or 6 for... loops and I'm really close to getting it working, but my code is horrible and I'm sure there's a better way of doing it. All I'm really doing is merging 2 arrays :o(

Any help gratefully received :o)

here's my current code:

var o;

//get the wedges from the previous animation call (the "old" array)
anWedges=gs.wedges[this._drawCall-1].clone();

//now find the new members:
//first set our new array to the current data:
var wedgesNew=gs.wedges[this._drawCall].clone();
//set "precedes" and "follows" properties:
for (w=0; w<wedgesNew.length; w++){
    wedgesNew[w].precedes=null;
    wedgesNew[w].follows=null;
    if (w>0){
        wedgesNew[w].follows=wedgesNew[w-1].id;
    }
    if(w<wedgesNew.length-1){
        wedgesNew[w].precedes=wedgesNew[w+1].id;
    }
}

//remove any members from our new array that were in the old one:
for (o=0; o<anWedges.length; o++){
    anWedges[o].new=false;
    anWedges[o].moved=false;
    for (w=0; w<wedgesNew.length; w++){
        if (anWedges[o].id===wedgesNew[w].id){
            wedgesNew.splice(w,1);
            break;
        }
    }
}

//add our new array members to the old array:
for (w=0; w<wedgesNew.length; w++){
    wedgesNew[w].new=true;
    //if its "follow" member doesn't exist, put it at the front:
    if (wedgesNew[w].follows===null){
        anWedges.unshift(wedgesNew[w]);
    }
    else{
        //otherwise, put it where it should be:
        for (o=0; o<anWedges.length; o++){
            if (anWedges[o].id===wedgesNew[w].follows){
                anWedges.splice(o+1, 0, wedgesNew[w]);
                break;
            }
        }
    }
}

//add a "follows" property to any members that don't have one:
for (o=0; o<anWedges.length; o++){
    if (isNil(anWedges[o].follows) && o > 0){
        for (w =0; w<gs.wedges[this._drawCall].length-1; w++){
            if (gs.wedges[this._drawCall][w+1].id === anWedges[o].id){
                anWedges[o].follows=gs.wedges[this._drawCall][w].id;
            }
        }
    }
}

//see if we need to move any members:
var prevID="";
for (o=0; o<anWedges.length; o++){
    var wRequired=false;
    for (w=0; w<gs.wedges[this._drawCall].length;w++){
        if (gs.wedges[this._drawCall][w].id===anWedges[o].id){
            wRequired=true;
            break;
        }
    }
    if (!wRequired){
        anWedges[o].removeMe=true;
    }
    else{
        if (o>0 && !anWedges[o].new){
            if(prevID!=="" && anWedges[o].follows!==prevID && !anWedges[o].removeMe){
                var wMoved=cloneObj(anWedges[o]);
                wMoved.moved=true;
                anWedges[o].removeMe=true;
                anWedges[o].id=anWedges[o].id+"_old";
                for (w=0;w<anWedges.length;w++){
                    if (anWedges[w].id===anWedges[o].follows){
                        anWedges.splice(w+1,0, wMoved);
                        break;
                    }
                }
            }
        }
        prevID=anWedges[o].id;
    }
}
MumbleBee
  • 21
  • 4

1 Answers1

0

Okay, I've sorted it.

I used a variation of David Furlong's unique function from this question to merge both arrays:

//returns a NEW array with duplicate members removed
Array.prototype.unique = function(prop) {
    var a = this.concat();
    for(var i=0; i<a.length; ++i) {
        for(var j=i+1; j<a.length; ++j) {
            if (!isNil(prop)){
                if (a[i].hasOwnProperty(prop) && a[j].hasOwnProperty(prop)) {
                    if(a[i][prop] === a[j][prop]){
                        a.splice(j--, 1);
                    }
                }
            }
            else{
                if(eve.isEqual(a[i], a[j])){
                    a.splice(j--, 1);
                }
            }
        }
    }
    return a;
};

and this function to sort the arrays of objects by their preferred index:

//sort an array of objects by an object property
function sortByProperty(prop) {
    return function (a,b) {
        if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop)) {
            return (a[prop] < b[prop]) ? -1 : (a[prop] > b[prop]) ? 1 : 0;
        }
    };
}

and my code is now much more succinct:

var oldWedges=gs.wedges[this._drawCall-1].clone();
var newWedges=gs.wedges[this._drawCall].clone();

//merge the old and new arrays, omitting duplicate IDs (start with the new array, that way we honour the new order):
var anWedges = newWedges.concat(oldWedges).unique("id");
//tag any new members as new:
for (n=0; n<anWedges.length;n++){
    if (oldWedges.indexOfProp("id", anWedges[n].id) === -1){
        anWedges[n].new=true;
    }
}

//check that the order of the old members hasn't changed
//if it has, tag them for moving:
for (o=0; o<oldWedges.length;o++){
    n = anWedges.indexOfProp("id", oldWedges[o].id);
    var t=0;
    var tmp = null;
    var prev=null;
    var next=null;
    if (o>0){
        t=o-1;
        tmp=oldWedges[t];
        while (t>0 && anWedges.indexOfProp("id", tmp.id) === -1){
            tmp=oldWedges[t];
            t-=1;
        }
        prev=tmp;
    }
    if (o<oldWedges.length-1){
        t=o+1;
        tmp=oldWedges[t];
        while (t<oldWedges.length-1 && anWedges.indexOfProp("id", tmp.id) === -1){
            tmp=oldWedges[t];
            t+=1;
        }
        next=tmp;
    }
    if (!isNil(prev)){
        if (prev.i>=anWedges[n].i){
            anWedges[n].moveMe=true;
        }
    }
    if (!isNil(next)){
        if (next.i<=anWedges[n].i){
            anWedges[n].moveMe=true;
        }
    }
}

//create duplicate wedges for any that need to move so that we can animate them collapsing:
for (n=0;n<anWedges.length;n++){
    if (anWedges[n].moveMe){
        anWedges[n].moveMe=false;
        var oldWedge=cloneObj(oldWedges[oldWedges.indexOfProp("id", anWedges[n].id)]);
        oldWedge.sector=oldWedge.sector.clone();
        var oldIndex=0;
        if (oldWedges.indexOfProp("id", oldWedge.id)>0){
            var prevID=oldWedges[oldWedges.indexOfProp("id", oldWedge.id)-1].id;
            oldIndex=anWedges.indexOfProp("id", prevID)+1;
        }
        oldWedge.id+="_old";
        oldWedge.removeMe=true;
        anWedges[n].moved=true;
        anWedges.splice(oldIndex, 0, cloneObj(oldWedge));
    }
}

//tag any old wedges that are obsolete for removal
for (n=0;n<anWedges.length;n++){
    if (newWedges.indexOfProp("id", anWedges[n].id)===-1){
        anWedges[n].removeMe=true;
    }
}

//sort wedges by their preferred order:
anWedges.sort(sortByProperty("i"));

It's still not as tidy as I'd like, but it works on all my test cases. :o)

MumbleBee
  • 21
  • 4