Custom Drawing Tools

This document outlines how to create a new drawing object.

We will also demonstrate how to add and remove drawings programtically.

For simple lines you can use STXChart#plotLine or STXChart#plotSpline.

Diamond example

The first thing I want to do is figure out how I would tell the application to create this drawing as simply as possible. As you know, the diamond has 4 corners, each a right angle. In addition, each side of the diamond is of equal length. So, I can create the diamond by choosing just 2 corners. The application should be able to figure out how to draw the drawing with the minimal information of 2 vertices. Let's say I want to choose the 2 corners opposite each other. Based on that, the diamond will be drawn.

Now that we know what we are drawing, we can begin to create the class for the drawing. Let's start with the constructor:


        STX.Drawing.diamond=function(){
            this.name="diamond";
            this.dragToDraw=true;
        };

        STX.Drawing.diamond.stxInheritsFrom(STX.Drawing.BaseTwoPoint);

What this does is create a drawing called "diamond" which inherits from BaseTwoPoint, a simple abstract drawing which takes 2 points. We can fall back on any function already defined in the BaseTwoPoint class, or we can redefine it. We will be redefining some of these functions. The dragToDraw property allows us to choose the first vertex by clicking down on the mouse (or pressing down on the touch device), and choose the second one by releasing the mouse button (or removing the finger from the touch device). When dragToDraw is set to false, the both vertexes are chosen by clicking the mouse for each vertex (or briefly tapping the touch device for each vertex).

Next, we will define the serialize and reconstruct functions. Serialize puts the parameters used to create the drawing into an object that is then transformed into a string downstream. Reconstruct is the opposite. It takes an object and copies the properties of the object into its own class properties. Here is the serialize function for our diamond:


        STX.Drawing.diamond.prototype.serialize=function(){
            return {
                name:this.name,
                pnl: this.panelName,
                col:this.color,
                fc:this.fillColor,
                ptrn:this.pattern,
                lw:this.lineWidth,
                d0:this.d0,
                d1:this.d1,
                tzo0:this.tzo0,
                tzo1:this.tzo1,
                v0:this.v0,
                v1:this.v1
            };
        };

Some things to note here:

  1. name and panelName are required for all drawings.
  2. The object properties being created have very short names. This is so that the transformation to a string yields as few bytes as possible.
  3. The color, fillColor, pattern, and lineWidth are chosen from the drawing toolbar. We will look at that more closely later.
  4. The d0 and d1 represent the dates of the 2 vertices. The dates lie along the x-axis. In order to plot these, we will need to convert them into coordinates in real time. The tz0 and tz1 represent the time zone offset, in minutes, of the dates from UTC time. These are required if supporting accessing these drawings from multiple time zones.
  5. Similarly, the v0 and v1 are y-axis values for the 2 vertices. We will convert these into pixels in real time as well.
  6. A more complicated drawing may need more properties to be set in the object.

Here is the reconstruct function:


        STX.Drawing.diamond.prototype.reconstruct=function(stx, obj){
            this.stx=stx;
            this.panelName=obj.pnl;
            this.color=obj.col;
            this.fillColor=obj.fc;
            this.pattern=obj.ptrn;
            this.lineWidth=obj.lw;
            this.d0=obj.d0;
            this.d1=obj.d1;
            this.tzo0=obj.tzo0;
            this.tzo1=obj.tzo1;
            this.v0=obj.v0;
            this.v1=obj.v1;
            this.adjust();
        };

This seems to be the reverse of serialize, except we don't need to assign the name, since that was retrieved earlier and used to call our class's reconstruct function.

The next step is to create the render function. This function is responsible for drawing the object on the canvas. We'll start out with the basics. Here is the beginning of the function:


        STX.Drawing.diamond.prototype.render=function(context){
            var panel=this.stx.panels[this.panelName];
            if(!panel) return;
            var x0=this.stx.pixelFromTick(this.p0[0], panel.chart);
            var x1=this.stx.pixelFromTick(this.p1[0], panel.chart);
            var y0=this.stx.pixelFromValueAdjusted(panel, this.p0[0], this.p0[1]);
            var y1=this.stx.pixelFromValueAdjusted(panel, this.p1[0], this.p1[1]);
            ...
            ...
            ...
        };

First we get a reference to the panel which contains the drawing. Then we get the x and y coordinates on the canvas (in pixels) using the conversion functions pixelFromTick() which converts the date into x coord, and pixelFromValueAdjusted, which converts the y-intercept into the y coord. Note some interesting things. First, we are using p0 and p1. Those are convenience arrays which are equivalent to [d0,v0] and [d1,v1] where d0 and d1 are timezone aware. So, for example, when we reference p1[0], we really mean d1. Also note how pixelFromValueAdjusted needs the x axis value (the date) as well as the y axis value in order to determine the y coordinate. This is because the value may be adjusted for dividends or splits, and we need to know the date the y value is occurring so we can adjust appropriately.

So at this point, we have the canvas coordinates for our 2 points. Next, within our render function (where the ellipses are above), we will establish the borders of our object so we can draw it and fill it in.


            var midpoint=[(x0+x1)/2,(y0+y1)/2];
            v3=[midpoint[0]+(y1-y0)/2,midpoint[1]-(x1-x0)/2];
            v4=[midpoint[0]-(y1-y0)/2,midpoint[1]+(x1-x0)/2];
             //Now we fill in the interior:
            var edgeColor=this.color;
            var fillColor=this.fillColor;
            if(fillColor && !STX.isTransparent(fillColor) && fillColor!="auto"){
                context.beginPath();
                context.moveTo(x0,y0);
                context.lineTo(v3[0],v3[1]);
                context.lineTo(x1,y1);
                context.lineTo(v4[0],v4[1]);
                context.fillStyle=fillColor;
                context.globalAlpha=.2;
                context.fill();
                context.closePath();
                context.globalAlpha=1;
            }
            var parameters={
                pattern: this.pattern,
                lineWidth: this.lineWidth
            };
            if(this.highlighted && parameters.pattern=="none"){
                parameters.pattern="solid";
                if(parameters.lineWidth==.1) parameters.lineWidth=1;
            }

            // We extend the vertical lines by .5 to account for displacement of the horizontal lines
            // HTML5 Canvas exists *between* pixels, not on pixels, so draw on .5 to get crisp lines                    
             this.stx.plotLine(x0,v3[0],y0,v3[1],edgeColor,"diamond",context,panel,parameters);
            this.stx.plotLine(v3[0],x1,v3[1]-.5,y1+.5,edgeColor,"diamond",context,panel,parameters);
            this.stx.plotLine(x1,v4[0],y1,v4[1],edgeColor,"diamond",context,panel,parameters);
            this.stx.plotLine(v4[0],x0,v4[1]+.5,y0-.5,edgeColor,"diamond",context,panel,parameters);

            ...
            ...

Note I still have ellipses at the bottom of my render function, well get back to those later. Here are some overhead functions that need to be defined:


        STX.Drawing.diamond.prototype.copyConfig=function(){
            this.color=STXChart.currentColor;
            this.fillColor=STXChart.currentVectorParameters.fillColor;
            this.lineWidth=STXChart.currentVectorParameters.lineWidth;
            this.pattern=STXChart.currentVectorParameters.pattern;
        };

What does this function do? It allows you to use the options selected from the drawing toolbar for your new drawing. The drawing toolbar contains various controls such as color, fill color, pattern, and line width. We will use all of these for our diamond drawing, but we will not use axis label. Therefore, in the STX.DrawingToolbar.configurator, set "diamond":false property in the ".stxToolbarAxisLabel" object. We also want to add the new drawing tool to the dropdown on the web page. To do that, add the line:


        <li stxtoggle="STX.DrawingToolbar.setDrawingType('diamond', this);">Diamond</li>

to the


        <ul id="toolbarDraw" ...>

Next, there is the intersected function. This function is used to determine if your cursor is hovering on the drawing (so you can edit or delete it).


        STX.Drawing.diamond.prototype.intersected=function(tick, value, box){
            this.whichPoint=null;
            if(!this.p0 || !this.p1) return null; // in case invalid drawing (such as from panel that no longer exists)
            if(this.pointIntersection(this.p0[0], this.p0[1], box)){
                this.highlighted="p0";
                this.whichPoint="p0";
                return {
                    action: "drag",
                    point: "p0"
                };
            }else if(this.pointIntersection(this.p1[0], this.p1[1], box)){
                this.highlighted="p1";
                this.whichPoint="p1";
                return {
                    action: "drag",
                    point: "p1"
                };
            }
            if(this.boxIntersection(tick, value)){
                this.highlighted=true;
                return {
                    action: "move",
                    p0: STX.clone(this.p0),
                    p1: STX.clone(this.p1),
                    tick: tick,
                    value: value
                };
            }
            return null;
        };

This function checks if you are hovering above one of the two points used to create the diamond. If you are, you will be able to reposition the drawing. That is what the pointIntersection drawing does. The boxIntersection function checks to see if the coordinates of the click are within the boundaries of the drawing. If they are, you will be able to move or delete the drawing. The formula for determining whether coordinates are within a diamond is beyond the scope of this tutorial; suffice it to say the boxIntersection function should return a true/false. If boxIntersection is not implemented for a diamond, the default will check if the coordinates are within a rectangle bounded on opposite corners by the two vertices chosen to draw the shape.

Finally, to create some handles for the repositioning, we will need to add the last part to the render function:


            if(this.highlighted){
                var p0Fill=this.whichPoint=="p0"?true:false;
                var p1Fill=this.whichPoint=="p1"?true:false;
                this.littleCircle(context, x0, y0, p0Fill);
                this.littleCircle(context, x1, y1, p1Fill);
            }

One final note:

Should you find yourself needing so set p0 and p1 in your drawings, you should use the setPoint method:


    this.setPoint(i, x, y, chart);
  • i is the p index you want to set (0 or 1).
  • x can be either a date or a tick.
  • y is a value.
  • chart is the chart within the panel you want to draw in (usually pass in panel.chart).

The advantage of using this method rather than setting p0 or p1 directly is that it automatically converts ticks to dates and vice versa, as well as reads your timezone and stores it in the date. You shouldn't normally need to manipulate d0/d1 or v0/v1.

Programatically adding and removing drawings

For simple lines you can use STXChart#plotLine or STXChart#plotSpline.

Adding a drawing

If you already have a serialized list (array) of drawing renditions and wish to programatically add them to the chart, you can do this by calling STXChart#reconstructDrawings. This will create the appropriate drawing objects and adds it to STXChart#drawingObjects

This function can be used to add one drawing at a time or a group all at once, and can be used multiple times to add more drawings to the chart as needed. Once you are done adding the drawings, you must call STXChart#draw to display the drawings.

Example:

// programatically add a rectangle and a line
 stxx.reconstructDrawing(
     [
          {
              "name":"rectangle",
              "pnl":"chart",
              "col":"transparent",
              "fc":"#7DA6F5",
              "ptrn":"solid",
              "lw":1.1,
              "d0":"20151216030000000",
              "d1":"20151216081000000",
              "tzo0":300,
              "tzo1":300,
              "v0":152.5508906882591,
              "v1":143.3385829959514
          },
          {
            "name":"vertical",
            "pnl":"chart",
            "col":"transparent",
            "ptrn":"solid",
            "lw":1.1,
            "v0":147.45987854251013,
            "d0":"20151216023000000",
            "tzo0":300,
            "al":true
          }
    ]
);

// now render the reconstructed drawings
stxx.draw();

Alternatively, if all you want to do is add a single drawing, you can do this by calling STXChart#createDrawing, which will automatically draw it on the chart.

Example:

stxx.createDrawing(
    "vertical", 
    {
        "pnl":"chart",
        "col":"transparent",
        "ptrn":"solid",
        "lw":1.1,
        "v0":147.45987854251013,
        "d0":"20151216023000000",
        "tzo0":300,
        "al":true
    }
);

Note that the reconstructDrawings function requires the "name" (drawing type) parameter to be included in the serialization list, otherwise it will not know what type of drawing is being requested. createDrawing, on the other hand, takes the drawing type as a separate parameter since you are specifically requesting that type of drawing to be created.

Removing a drawing

To remove a drawing call STXChart#removeDrawing

For example, to remove the first drawing added to the chart, you would do the following:

stxx.removeDrawing(stxx.drawingObjects[0]);

Remember that STXChart#drawingObjects contains the complete list of drawings on the chart, and you can iterate trough it removing any one you wish.