Sunday, 26 March 2017

Paths and Choropleth Maps and Globes using D3.js

Drawing Paths, Choropleth Globes and Maps using the D3.js library!






The above video shows

  • flight paths ( plane shaped white svgs following arc paths ), 
  • yellow circles representing cities and their size proportionate to the city population
  • white-to-blue choropleth map of countries with the shade of blue based on the country Id
  • green dots showing earthquake points.

Checkout the above solution in action in this here.

The full source code can be found on github.


Spot Matrix / Corrplots using D3.js

Implementation of spot matrix using the D3.js library?
















Checkout the above solution in action in this tool.

The full source code can be found on github.


Atoms and Molecules using D3.js

How to draw atoms and molecules using the super awesome D3.js library?


Checkout the above solution in action in this interactive visualization tool.

The full source code can be found on github.
































Mike Bostock's this example demonstrated a hacky way to add multiple links between 2 nodes. His approach when novel for a double bond will not easily scale for triple, quadruple bonds.


So, how to render higher bonds in a scalable and neat way?


The trick: The x, y positions of the links have to be manually updated in the "tick" functionality of a force directed graph.

The general idea to solve this problem is to offset the links from the center to an amount in lieu to the orientation of the bond.

Which means at each instance of the link orientation, the slope of the link must be calculated and the x, y components will accordingly be offseted.

Have scale functions for X and Y axes.

    var thetaXScale = d3.scale.linear()
        .range([0.1, this.bondThickness])
        .domain([0, 90]);

    var thetaYScale = d3.scale.linear()

        .range([this.bondThickness, 0.1])
        .domain([0, 90]);

A helper function which computes the slope of the line.

    var compute_angle = function(x1, y1, x2, y2) {
        var tan_theta = (parseFloat(y2) - parseFloat(y1)) / (parseFloat(x2) - parseFloat(x1));
        var slope = Math.atan(tan_theta);
        return slope;
    }

The function which is called in the tick function of the force directed graph which returns the translate string with the x and y components.

The parameters to this function are 'd' which is the link data; 'dir' is the direction which means whether the link is to be offsetted to the left or right to the central line. Finally, 'bond_type' is whether the bond is single, double, triple, quadruple.

    var compute_translation = function(d, dir, bond_type,) {
        var x = 0;
        var y = 0;
        var slope = compute_angle(d.source.x, d.source.y, d.target.x, d.target.y) * 180 / Math.PI;

        if (dir == 'left' && slope < 0) {
            x = bond_type == "triple" ? -thetaXScale(Math.abs(slope)) * 3 : -thetaXScale(Math.abs(slope)) * 2;
            y = bond_type == "triple" ? -thetaYScale(Math.abs(slope)) * 3 : -thetaYScale(Math.abs(slope)) * 2;
        } else if (dir == 'left' && slope > 0) {
            x = bond_type == "triple" ? thetaXScale(Math.abs(slope)) * 3 : thetaXScale(Math.abs(slope)) * 2;
            y = bond_type == "triple" ? -thetaYScale(Math.abs(slope)) * 3 : -thetaYScale(Math.abs(slope)) * 2;
        } else if (dir == 'right' && slope < 0) {
            x = bond_type == "triple" ? thetaXScale(Math.abs(slope)) * 3 : thetaXScale(Math.abs(slope)) * 2;
            y = bond_type == "triple" ? thetaYScale(Math.abs(slope)) * 3 : thetaYScale(Math.abs(slope)) * 2;
        } else if (dir == 'right' && slope > 0) {
            x = bond_type == "triple" ? -thetaXScale(Math.abs(slope)) * 3 : -thetaXScale(Math.abs(slope)) * 2;
            y = bond_type == "triple" ? thetaYScale(Math.abs(slope)) * 3 : thetaYScale(Math.abs(slope)) * 2;
        } else if (dir == 'left2' && slope < 0) {
            x = -thetaXScale(Math.abs(slope)) * 6;
            y = -thetaYScale(Math.abs(slope)) * 6;
        } else if (dir == 'left2' && slope > 0) {
            x = thetaXScale(Math.abs(slope)) * 6;
            y = -thetaYScale(Math.abs(slope)) * 6;
        } else if (dir == 'right2' && slope < 0) {
            x = thetaXScale(Math.abs(slope)) * 6;
            y = thetaYScale(Math.abs(slope)) * 6;
        } else if (dir == 'right2' && slope > 0) {
            x = -thetaXScale(Math.abs(slope)) * 6;
            y = thetaYScale(Math.abs(slope)) * 6;
        }
        return "translate(" + x + "," + y + ")";
    } 

I have not only implemented double, triple or quadruple bonds. 
What if different types of links (bonds) are to be rendered. For example, in chemistry, there are bonds like -
1) Dashed bonds (triangular shaped striped filled meaning bond is into-the-paper)
2) Wedged Bonds (triangular shaped solid filled meaning bond is out-of-paper)

The above logic is applicable to these type of bonds too. Only one side (either source or the destination) is to be offsetted left and right of the central line. And we will get a triangular bond.

Thanks.

Kagi Charts using D3.js

Have you heard about Kagi charts?

It is a chart used for tracking price movements and to make decisions on purchasing stock. It differs from traditional stock charts such as the Candlestick chart by being mostly independent of time. This feature aids in producing a chart that reduces random noise.

We are going to try to render kagi charts from sample data. The end result will be something like:





You can find the complete source code on github.

An example implementation here


The varying thickness (or colour) of the line is dependent on the price behaviour. When the price goes higher than a previous "shoulder" reversal, the line becomes thicker (and/or green) and is known as a "Yang line". This can be interpreted as an increase in demand over supply for the asset and as a bullish upward trend. Alternatively, when the price breaks below a previous "waist" reversal, the line becomes thinner (and/or red) and is known as a "Yin line". This signifies an increase in supply over demand for the asset and as a bearish downward price trend.

The algorithm to render Kagi charts goes like this:-


  1. Find the starting point. The starting point is generally considered the first closing price. From this point forward, you compare each day's closing price with the starting price.
  2. Draw a thin vertical line from the starting price to each day's closing price, while the trend does not reverse.
  3. If a day's closing price moves in the opposite direction to the trend by more than the reversal amount, draw a short horizontal line and a new vertical line, beginning from the horizontal line to the new closing price.
  4. If the price on a day is greater than or equal to the previous high, change to a thick line and continue the vertical line. If the price on that day is less than or equal to the previous low, then change to a thin line.

Let's begin...

1. Finding the starting point and the initial point.

// Figure out the "initial trend" in datafor(var k=1;k<temp_array.length;k++){
    var diff = temp_array[k].close - temp_array[k-1].close;
    if (diff>0){
        trend = '+';
        broke_at = k;
        break;
    }else if(diff<0){
        trend = '-';
        broke_at = k;
        break;
    }else{
        continue;
    }
}

In D3.js, it is not possible to have 1 line with varying stroke-widths or stroke-colours. 
One solution is to use svg's linearGradient but that is not a very scalable and neat solution. 

Therefore, the only work around is to draw multiple lines connecting all the points which are calculated by the Kagi algorithm.

2. Kagi charts reversal happens based on 2 ways - simple reversal value or percentage.


var value_to_compare = 0;
if(reversalType.localeCompare("diff")){
    value_to_compare = diff; 
    //If reversalType is difference then compare the change in value}else{
    value_to_compare = diff/last_close * 100; 
    //If reversalType is pct then compare the pct change in value}

3. It is after this, that we actually compare the set reversal value with the  value to compare.

       // If the absolute value of change (be it difference or percentage) is greater than the configured reversal_value
       if (Math.abs(value_to_compare) >= reversalValue){
           // means there is a change in trend. time to move along the x axis
           if(trends[i] != trends[i-1]){
               counter = counter+1;
               // Push the last_close at the new x position so a |_| or |-| kind of graph.
               output_data.push({x:counter,close:last_close,date:data[i].date});
               // Push the new close at the new x position
               output_data.push({x:counter,close:data[i].close,date:data[i].date});
           }
           // means there is no change in trend. time to move along the y axis (upward or downward)
           else{
                if(trends[i]=='+' && data[i].close>data[i-1].close){
                    output_data.push({x:counter,close:data[i].close,date:data[i].date});
                }
                else if(trends[i]=='-' && data[i].close < data[i-1].close){
                    output_data.push({x:counter,close:data[i].close,date:data[i].date});
                }
            }
           last_close = data[i].close;
           j=0;
       }else{
            if(trends[i]==trends[i-1]){
                // If the trend is the same and the last_close values are conforming to the trend, then
                // push to output_data in a way that it extends along the y axis on the same x axis point (counter).
                if(trends[i]=='+' && data[i].close>data[i-1].close){
                    output_data.push({x:counter,close:data[i].close,date:data[i].date});
                }
                else if(trends[i]=='-' && data[i].close < data[i-1].close){
                    output_data.push({x:counter,close:data[i].close,date:data[i].date});
                }
                // Safe to set the last_close here as it is an actual point added to output_data.
                last_close = data[i].close;
                // Reset the interim j variable to 0
                // Means the original dataset and output_data set are back in sync.
                j=0;
            }else{
                // This is to ignore minor variations in the stock values. We reset the last_close and current trend
                // every time this piece of code gets executed.
                // In Kagi charts, minor fluctuations are ignored while plotting.
                // The output_data set and the original dataset are out of sync till j != 0.
                last_close = data[i-1-j].close;
                trends[i] = trends[i-1-j];
                j+=1;
            }
        }

4. Then comes a filtering step. In this, since all we need are the points at the base and shoulders, all the intermediate points are redundant. Hence, one for loop and we filter these out.

function filter_same_x_points_from_data(data){
    var filtered_data = [];

    // Push the first datapoint

    filtered_data.push(data[0]);

    // If there are multiple points with the same x coordinate then filter the dataset to

    // have only the first and the last x position (highest and lowest last_close position)
    // This will remove considerable no. of points
    for(var i=1; i<data.length; i++){
        if(data[i].x == data[i-1].x){
            // ignore these points. this was exactly the purpose of this filtering function.
        }else{
            filtered_data.push(data[i-1]);
            filtered_data.push(data[i]);
        }
    }

    // Push the last datapoint

    filtered_data.push(data[data.length-1]);
    return filtered_data;
}

4. Great stuff. We're almost there.
To have the yang and yin lines with different lines, we need the break points from/to these lines originate and end. The below function snippet aids in doing that.

// This function add the points which are at the shoulders and bases (useful only during change of trends).
function add_base_shoulder_points(data){
    var base;
    var shoulder;
    var uptrend;

    // Deciding the initial trend in dataset based on which the base and shoulders are decided.
    if(data[1].close >= data[0].close){
        base = data[0].close;
        shoulder = data[1].close;
        uptrend=true;
    }else{
        base = data[1].close;
        shoulder = data[0].close;
        uptrend=false;
    }

    var points_to_add=[]; // abstracted out so can be used if needed.
    var positions_to_add_to = []; // abstracted out so can be used if needed.

    for(var i=0; i<data.length;i++){

        if(uptrend && data[i].close < base){
            // to_break:true is an identifier that the lines need to change their formatting beyond this point.
            points_to_add.push({date:data[i].date,close:base,x:data[i].x,to_break:true});
            positions_to_add_to.push(i);
            uptrend = !uptrend;
        }
        else if(!uptrend && data[i].close > shoulder){
            // to_break:true is an identifier that the lines need to change their formatting beyond this point.
            points_to_add.push({date:data[i].date,close:shoulder,x:data[i].x,to_break:true});
            positions_to_add_to.push(i);
            uptrend = !uptrend;
        }

        // Update the base and the shoulders while traversing the array.
        if(i>0 && data[i].close > data[i-1].close){
            base = data[i-1].close;
            shoulder = data[i].close;

        }else if(i>0 && data[i].close < data[i-1].close){
            base = data[i].close;
            shoulder = data[i-1].close;
        }
    }

    // Based on the points generated above and their positions,
    // actually add these points into the dataset for final yang-ying generation
    for(var k=0; k<positions_to_add_to.length; k++){
        // the +k is to encounter dynamic increase in the dataset's size.
        // The points_to_add need to be added at the correct position in the data array.
        data.splice(positions_to_add_to[k]+k,0,points_to_add[k]);
    }

    return data;
}

5. Let's set the style parameters of the yang, yin lines now :-

// This function makes the dataset which is fed to the d3.js library to render as svg
// Here, based on to_break metric, formatting options like thickness, colors are added.
function generate_yang_ying_lines(data,rallyThickness,declineThickness,rallyColor,declineColor){

    var output_array_of_lines = [];

    var start_position = 0;
    var break_position = 0;
    var uptrend;

    // Find the initial trend in data. In this case if its equal then I'm considering it as

    // positive.
    if(data[1].close >= data[0].close){
        uptrend = true;
    }else{
        uptrend = false;
    }

    // if the key "to_break" is true, then group the lines and add their formatting.

    for(var i=0;i<data.length;i++){
        if('to_break' in data[i]){
            var temp_array = data.slice();
            break_position = i;
            var lines = temp_array.splice(start_position,break_position-start_position+1);
            start_position = break_position;
            output_array_of_lines.push({uptrend:uptrend,p:lines,w:uptrend?rallyThickness:declineThickness,c:uptrend?rallyColor:declineColor});
            uptrend=!uptrend;
        }
    }

    // adding the last set of lines.

    var temp_array = data.slice();
    var final_section_after_break = temp_array.splice(start_position);
    output_array_of_lines.push({p:final_section_after_break,w:uptrend?rallyThickness:declineThickness,c:uptrend?rallyColor:declineColor});

    return output_array_of_lines;

}

6. Rendering the lines now:-

    // Add the valueline path.
    var path  = svg.selectAll('path')
        .data(data)
        .enter()
        .append("path")
        .attr("class", "line")
        .attr("d", function(d){return valueline(d.p);}) // line group
        .style('stroke-width', function(d) { return d.w; }) //stroke-widths
        .style('stroke', function(d) { return d.c; }) // stroke-colors

7. Voila. The lines will get rendered as you wanted them to.