Building a candlestick chart (stock chart)

We are just about to make a super leap. Until now we worked with charts that had one data point, two data points, and a few variations on them, and now we are moving into a new world of four data points in every bar. The stock chart is a way to showcase changes in the market in a given time frame (in our example this is one day). Each day stock prices change many times, but the most important factors are the low and high values of the day and the opening and closing prices. A stock analyst needs to be able to see the information quickly and understand overall trends. We are skipping three data points elements, but we will be back to them in the recipe Building a bubble chart in Chapter 4, Let's Curve Things Up.

Building a candlestick chart (stock chart)

The worst thing you can do is to assume that the only usage of four dimensions of data is in the stock market. This is where you can come up with the next big thing. Visualizing data in a clean and quick way and converting data into logic is one of the most fun things about charts. With that said let's start creating our stock chart.

Getting ready

Our first step is going to be a bit different in this recipe. I've created a sample CSV file called DJI.txt; you can find it in our source files. The format is the standard CSV format and the first line names all the data columns:

DATE,CLOSE,HIGH,LOW,OPEN,VOLUME

And all future lines contain the data (daily data in our case):

1309752000000,12479.88,12506.22,12446.05,12505.99,128662688

So the steps we will need to go through are loading the file, converting the data to fit our standard data set, and then build the new chart type (and then fixing things as we discover issues; agile development).

How to do it...

We are going to base our work starting from where we left in the previous recipe. We will start the modifications right in the JavaScript file:

  1. Let's update our global variables:
    var chartInfo= { y:{min:11500, max:12900,steps:5,label:"close"},
            x:{min:1, max:12, steps:11,label:"date"}
            };
    var stockData;
    var CHART_PADDING = 20;
    var wid;
    var hei
  2. Before we start our internal logic, we need to load our new external CSV file. We will rename the init function and call it startUp and then create a new init function:
    function init(){
    
      var client = new XMLHttpRequest();
      client.open('GET', 'data/DJI.txt'),
      
      client.onreadystatechange = function(e) {
       if(e.target.readyState==4){
    
         var aStockInfo = e.target.responseText.split("
    ");
         stockData = translateCSV(aStockInfo,7);
      
          startUp()
      
       }
      }
    
      client.send();
    }
    
    function startUp(){
      //old init function
    }
  3. The data we get back from the CSV file needs to be formatted to a structure we can work with. For that we create the translateCSV function that takes in the raw CSV data and converts it into an object that matches our architecture needs:
    function translateCSV(data,startIndex){
      startIndex|=1; //if nothing set set to 1
      var newData = [];
      var aCurrent;
      var dataDate;
      for(var i=startIndex; i<data.length;i++){
        aCurrent = data[i].split(",");
        dataDate = aCurrent[0].charAt(0)=="a"?parseInt(aCurrent[0].slice(1)):parseInt(aCurrent[0]);
        newData.push({	date:dataDate,
                close:parseFloat(aCurrent[1]),
                high:parseFloat(aCurrent[2]),
                low:parseFloat(aCurrent[3]),
                open:parseFloat(aCurrent[4]),
                volume:parseFloat(aCurrent[5])
                });	
      }
    
      return newData;	
    }
  4. Our startUp function, formally known as init, will remain the same besides changing the createWaterfall method to call addStock:
    function startUp(){
      ...
      addStock(context,stockData);	
    }
  5. It is time to create the addStock function:
    function addStock(context,data){ 
      fillChart(context,chartInfo);
      var elementWidth =(wid-CHART_PADDING*2)/ data.length;
      var startY = CHART_PADDING;
      var endY = hei-CHART_PADDING;
      var chartHeight = endY-startY;
      var stepSize = chartHeight/(chartInfo.y.max-chartInfo.y.min);
      var openY;
      var closeYOffset;
      var highY;
      var lowY;
      var currentX;
      context.strokeStyle = "#000000";
      for(i=0; i<data.length; i++){
        openY = (data[i].open-chartInfo.y.min)*stepSize;
        closeYOffset = (data[i].open-data[i].close)*stepSize;
        highY = (data[i].high-chartInfo.y.min)*stepSize;
        lowY =(data[i].low-chartInfo.y.min)*stepSize;
        context.beginPath();
        currentX = CHART_PADDING +elementWidth*(i+.5);
        context.moveTo(currentX,endY-highY);
        context.lineTo(currentX,endY-lowY);
        context.rect(CHART_PADDING +elementWidth*i ,endY-openY,elementWidth,closeYOffset);
        context.stroke();
        context.fillStyle = closeYOffset<0? "#C2D985" :"#E3675C" ;
        context.fillRect(CHART_PADDING +elementWidth*i ,endY-openY,elementWidth,closeYOffset);
      }
    
    }

All these steps are required to create a new candlestick chart.

How it works...

Let's review the steps to load our external file. If you are working with open source tools such as jQuery you will be better off using them to load external files but, to avoid using other libraries, we will work with the XMLHttpRequest object as it's supported in all modern browsers that support HTML5.

We start with creating a new XMLHttpRequest object:

var client = new XMLHttpRequest();
client.open('GET', 'data/DJI.txt'),

The next step is to set what we want to do (GET/POST) and the name of the file, followed by creating a handler function for the onreadystatechange callback and sending our request.

client.onreadystatechange = function(e) {
   if(e.target.readyState==4){
     var aStockInfo = e.target.responseText.split("
");
     stockData = translateCSV(aStockInfo,1);
      startUp()
  
   }
  }
  client.send();

The event handler onreadystatechange gets called a few times throughout the loading process of a file. We only want to listen in and act once the file is loaded and ready to be played with; to do that we will check whether the readyState variable is equal to four (ready and loaded). When the file is loaded, we want to split our file into an array based on line breaks.

Note

Note that the file was created on a Mac. The does the trick, but when you create your own files or download files, you might need to use or a combination or . Always confirm that you made the right selection by outputting the length of your array and validating its right size (then test to see if its content is what you expect it to be).

After our array is ready we want to format it to the user-friendly format followed by starting up the old init function that is now known as startUp.

Let's quickly review the translateCSV formatting function. We are literally looping through our data array that was created earlier and replacing each line with a formatted object that will work for our needs. Notice that we have an optional parameter startIndex. If nothing or zero is set then on the first line we are assigning it the value of 1:

startIndex||=1; //if nothing set set to 1

The former is a shorthand way of writing:

startIndex = startIndex || 1;

If the startIndex parameter has a value that is equivalent to true then it would remain as it was; if not, it would be converted to 1.

By the way, if you don't know how to work with these shortcuts, I really recommend getting familiar with them; they are really fun and save time and typing. If you want to learn more on this check the following links:

Great! Now we have a data source that is formatted in the style we've been using so far.

We will hardcode our chartInfo object. It will work out well for our y values but not that well for our date requirements (in the x values). We will revisit that issue later after we get our chart running. We created a dynamic range generator in an earlier exercise, so if you want to keep up with that then review it and add that type of logic into this chart as well, but for our needs we will keep it hardcoded for now.

Ok, so let's dig deeper into the addStock function. By the way, notice that as we are working with the same format and overall tools, we can mix charts together with ease. But before we do that, let's understand what the addStock function actually does. Let's start with our base variables:

  fillChart(context,chartInfo);
  var elementWidth =(wid-CHART_PADDING*2)/ data.length;
  var startY = CHART_PADDING;
  var endY = hei-CHART_PADDING;
  var chartHeight = endY-startY;
  var stepSize = chartHeight/(chartInfo.y.max-chartInfo.y.min);

We are gathering information that will make it easier to work in our loop when creating the bars from the width of elements (elementWidth to the ratio between our values and the height of our chart). All these variables have been covered in earlier recipes in this chapter.

  var openY;
  var closeYOffset;
  var highY;
  var lowY;
  var currentX;
  context.strokeStyle = "#000000";

These variables are going to be our helper variables (updated after every round of our loop) to establish the position of the high, low, open, and close offsets (as we are drawing a rectangle, it expects the height and not a second y value).

The first thing we do in each round of our loop is to find out the values for these variables:

for(i=0; i<data.length; i++){
    openY = (data[i].open-chartInfo.y.min)*stepSize;
    closeYOffset = (data[i].open-data[i].close)*stepSize;
    highY = (data[i].high-chartInfo.y.min)*stepSize;
    lowY =(data[i].low-chartInfo.y.min)*stepSize;

You will notice that the logic is almost the same in all of the variables. We are just subtracting the minimum from the value (as our chart doesn't cover values under our minimum value), and then multiplying it by our stepSize ratio to have the value fit within our chart dimensions (this way even if we change our chart size everything should continue working). Note that only the closeYOffset variable doesn't subtract the min property but instead it subtracts the close property.

The next step is to draw our candlestick chart starting with a line from the low to the high of the day:

    context.beginPath();
    currentX = CHART_PADDING +elementWidth*(i+.5);
    context.moveTo(currentX,endY-highY);
    context.lineTo(currentX,endY-lowY);

This will be followed by the rectangle that represents the full open and close values:

    context.rect(CHART_PADDING +elementWidth*i ,endY-openY,elementWidth,closeYOffset);
    context.stroke();
    context.fillStyle = closeYOffset<0? "#C2D985" :"#E3675C" ;
    context.fillRect(CHART_PADDING +elementWidth*i ,endY-openY,elementWidth,closeYOffset);
}

After this, we will create a fill to this rectangle and set the style color based on the value of the closeYOffset variable. At this stage we have a running application, although it can use a few more tweaks to make it work better.

There's more...

It's time to fix our x coordinate values:

var chartInfo= { y:{min:11500, max:12900,steps:5,label:"close"},
        x:{min:1, max:12, steps:11,label:"date"}
        };

We didn't change this variable before as until now there was a clear separation between the outline and our content (the chart itself); but at this stage as our x outline content isn't a linear number anymore but a date; we need to somehow introduce into the fillChart method external data that is related to the content of the chart. The biggest challenge here is that we don't want to introduce into this method something that is only relevant to our chart as this is a globally used function. Instead we want to put our unique data in an external function and send that function in as a formatter. So let's get started:

var chartInfo= { y:{min:11500, max:12900,steps:5,label:"close"},
        x:{label:"date",formatter:weeklyCapture}
        };

Our x space in a stock chart represents time and as such our previous usage based on linear data does not apply (the properties such as min, max, and steps have no meaning in this case). We will remove them in favor of a new property formatter that will take a function as its value. We will use this formatter function instead of the default function. If this function is set we will let an external function define the rules. We will see more on this when we describe the weeklyCapture function. This method of coding is called plugin coding . Its name is derived out of the idea of enabling us to create replaceable functions without reworking our core logic in the future. Before we create the weeklyCapture function, let's tweak the chartInfo object so we have the right range and number of steps:

function addStock(context,data){
  if(!chartInfo.x.max){
    chartInfo.x.min = 0;
    chartInfo.x.max = data.length;
    chartInfo.x.steps = data.length;	
  }

  fillChart(context,chartInfo);

...

What we are doing here is, before we call the fillChart function in our addStock function, we are checking to see if the max value is set; if it isn't set, we are going to reset all the values, setting the min to 0 and the max and steps to the length of our data array. We are doing this as we want to travel through all of our data elements to test and see if there is a new weekday.

Now we integrate our weeklyCapture function into the fillChart function.

function fillChart(context, chartInfo){
  // ....
  var output;
  for(var i=0; i<steps; i++){
    output = chartInfo.x.formatter && chartInfo.x.formatter(i);
    if(output || !chartInfo.x.formatter){
      currentX = startX + (i/steps) *	chartWidth;
      context.moveTo(currentX, startY );
      context.lineTo(currentX,endY);
      context.fillText(output?output:xData.min+stepSize*(i), currentX-6, endY+CHART_PADDING/2);
    }
  }
  
  if(!chartInfo.x.formatter){
    currentX = startX +	chartWidth;
    context.moveTo(currentX, startY );
    context.lineTo(currentX,endY);
    context.fillText(xData.max, currentX-3, endY+CHART_PADDING/2);
  }

  context.stroke();

}

In our first step, we are going to fetch the value that comes back from our formatter function.

output = chartInfo.x.formatter && chartInfo.x.formatter(i);

The logic is simple, we are checking to see if the formatter function exists and if it does we are calling it and sending the current value of i (as we are in the loop).

The next step is if our output isn't empty (negative or has a value equivalent to false) or if our output is empty but our formatter isn't active then render the data:

if(output || !chartInfo.x.formatter){
  currentX = startX + (i/steps) *	chartWidth;
  context.moveTo(currentX, startY );
  context.lineTo(currentX,endY);
  context.fillText(output?output:xData.min+stepSize*(i), currentX-6, endY+CHART_PADDING/2);
}

Only if we have an output from the formatter function and/or if the formatter function does not exist we don't need to do anything. As such we need the if statement to capture both the scenarios, if we do not have the if statement, then our output will not conform to our earlier recipes. The only content we are changing within this block of code is the fillText method. If we are working with our output, we want to use that for the text. If not, we want to keep the logic that was there before the same:

if(output || !chartInfo.x.formatter){
  currentX = startX + (i/steps) *	chartWidth;
  context.moveTo(currentX, startY );
  context.lineTo(currentX,endY);
  context.fillText(output?output:xData.min+stepSize*(i), currentX-6, endY+CHART_PADDING/2);
}

We have one last thing we need to cover before we can run our application and see it in action and that is to create our weeklyCapture function. So let's create it now:

var DAY = 1000*60*60*24;
function weeklyCapture(i){
  var d;
  if(i==0){
    d =  new Date(stockData[i].date);	
  }else if ( i>1 && stockData[i].date != stockData[i-1].date+1 ){
    d = new Date(stockData[i].date + DAY*stockData[i].date );
  }

  return d? d.getMonth()+1+"/"+d.getDate():false;

}

We start by creating a helper variable called DAY that will store how many milliseconds there are in a day:

var DAY = 1000*60*60*24;

If you take a peek at our external data, you will see that only on day 0 we have an actual date (formatted in milliseconds since 1970). All we need to do is send that to the date object to create a date:

var d;
  if(i==0){
    d =  new Date(stockData[i].date);	
  }

While all other data lines contain only a number that represents how many days passed since that original date, we want to test and see if the current date is only one day after the last day. Only if the date change is greater than one day, we will create a new date format for it:

}else if ( i>1 && stockData[i].date != stockData[i-1].date+1 ){
    d = new Date(stockData[0].date + DAY*stockData[i].date );
  }

Note that to create the date object we need to take our current original date from row 0 and then add to it the total days in milliseconds (multiplying our DAY variable with the current day value).

With this method all that is left to check is if we have a valid date. Let's format it and send it back, and if not, we will send back false:

return d? d.getMonth()+1+"/"+d.getDate():false;

Congratulations! Now our sample is a fully fledged integrated candlestick chart with live dynamic dates.

Adding other render options to our stock chart

Although the candlestick chart is a very popular option, there is another popular technical chart view. One that is used when there are no colors to use. Instead of the usage of colors, on the left-hand side we draw a line to define the opening price, and on the right-hand side we capture the closing price. Let's integrate that logic into our chart as an optional render mode. We will add a new parameter to the addStock function:

function addStock(context,data,isCandle){

We are now going to adjust our internal for loop to change the render depending on the value of this variable:

for(i=0; i<data.length; i++){
    openY = (data[i].open-chartInfo.y.min)*stepSize;
    closeYOffset = (data[i].open-data[i].close)*stepSize;
    highY = (data[i].high-chartInfo.y.min)*stepSize;
    lowY =(data[i].low-chartInfo.y.min)*stepSize;
    context.beginPath();
    currentX = CHART_PADDING +elementWidth*(i+.5);
    context.moveTo(currentX,endY-highY);
    context.lineTo(currentX,endY-lowY);
    if(!isCandle){
      context.moveTo(currentX,endY-openY);
      context.lineTo(CHART_PADDING +elementWidth*(i+.25),endY-openY);
      context.moveTo(currentX,endY-openY+closeYOffset);
      context.lineTo(CHART_PADDING +elementWidth*(i+.75),endY-openY+closeYOffset);
      context.stroke();
    }else{
      context.rect(CHART_PADDING +elementWidth*i ,endY-openY,elementWidth,closeYOffset);
      context.stroke();
      context.fillStyle = closeYOffset<0? "#C2D985" :"#E3675C" ;
      context.fillRect(CHART_PADDING +elementWidth*i ,endY-openY,elementWidth,closeYOffset);

    }

  }

There we go. We set the default to be false for our isCandle Boolean variable. If we run our application again, we will find it rendering in the new format. To change that, all we need to do is provide that third parameter as true when calling the addStock function:

Adding other render options to our stock chart

This chapter has been self-contained and really the hub of all the charts if you need to strengthen your chart building skills. I recommend you to revisit some of the earlier recipes in this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.85.183