OnGraphDraw

Revision as of 00:18, 12 January 2023 by Lchrisman (talk | contribs) (→‎See Also)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)



Release:

4.6  •  5.0  •  5.1  •  5.2  •  5.3  •  5.4  •  6.0  •  6.1  •  6.2  •  6.3  •  6.4  •  6.5


The OnGraphDraw attribute lets you annotate a graph with text, lines, areas, and other graphics -- for example, to add labels to points, or point out interesting aspects of a graph. It even lets you show a map or other image under a graph, and display points over it. annotations may depend on the values being graphed and so reposition themselves as the data changes. It also provides a hook where you can enhance graphing functionality (or make use of existing functions that do so) when there is something you want that the built-in graphing engine support.

Analytica (since 5.2 ) includes two standard libraries with functions to make it easy to use OnGraphDraw:

Additional libraries of functions that perform various types of annotations or graphs may become available over time, and may be contributed by other Analytica users.

See below for how to use OnGraphDraw and create your own functions.


New to Analytica 5.2

Attribute OnGraphDraw

You can write code in OnGraphDraw to draw nearly anything you can dream up. But you may want to use existing libraries mentioned above unless you are a power user.

The OnGraphDraw attribute of a variable may contain an expression -- e.g. a function call -- which is evaluated while a graph is being drawn. The expression may draw commands on a Canvas in front of or behind the standard graph plot.

This example labels the minimum and maximum points in a data series:

OnGraphDraw min and max.png

The graphing engine scaled and drew the axes and plotted the points. The code in the OnGraphDraw attribute found the min and max points and drew the annotations. (See how this was done).

In this example, OnGraphDraw gets a map from Google maps and draws it behind the plotted data. Here the X-Y graph has three data points connected by two lines:

OnGraphDraw map.png

The OnGraphDraw expression obtains the map from Google maps using ReadFromURL -- the map is not served by the graphing engine itself. The library function OnDraw_Google_map makes it easy to include graph on a map (Analytica Enterprise required) -- see OnDraw_Google_map for instructions.

Local variables in OnGraphDraw

An OnGraphDraw expression can access these local variables to get information about the graph and the current graphing roles:

  • canv: The canvas. This is what you draw to using the canvas drawing functions.
  • info: A vector of information about the graph, such as the dimensions of the graph and location of the plot area. The vector is indexed by the system index OnGraphDrawItem index.
  • phase: The drawing phase. This enables you to detect at what stage of the graph rendering the attribute is being evaluated. Possible values are: 1=Before graphing has started, 2=After the graph layout has been calculated, but before anything is drawn, 4=After the background and axes have been drawn, but before the data is plotted, 8=After the graph has been fully rendered (except for your own embellishments).
  • roles: Information about the graphing dimensions that fill each graphing role. This is indexed by GraphingRole and GraphFillerInfo.
  • continue: A boolean that you can set to false if you don't want the graph to be drawn any further beyond this point. This can be used to substitute your own custom drawing code in place of the built-in graphing engine, which might include totally different graphical depictions.
  • roleChanges: It is possible to alter axis scaling by changing items in this local. For example, you may need to adjust the axis range in order to register a map image so that data plotted on top appear at the correct latitude and longitude.

When OnGraphDraw is evaluated

By default, it evaluates the expression in OnGraphDraw after rendering the graph data. That's useful when you want to draw the annotations on the graph over the data. You can also set it to evaluate at three earlier phases of drawing. To set this preference, check the appropriate box in the OnGraphDraw attribute in the Object Window of the graphed variable:

OnGraphDraw-checkboxes.png


Note: These check boxes appear in the Object window, but not in the Attribute pane.

It evaluates OnGraphDraw during each phase of the graph rendering that you check:

  • 1: Evaluate before layout: Call before graphing has started, including before determining the layout info, such as the plot area. To replace a graph entirely with a custom graphical depiction of your own, enable this, and set continue:=0 in your OnGraphDraw code.
  • 2: Evaluate before drawing: Call after determining the layout, but before drawing anything, so annotations will appear behind any background and axes.
  • 4: Evaluate after axes, before data: Draw annotations after the background and axes, but behind data points, lines or bars.
  • 8 Evaluate after fully drawn (default) Call after the standard graphing has completed to annotate the graph or data with images in front of the data.

The check boxes in the object window are stored in the OnGraphDrawFlags attribute of the graphed object, using a value equal to the sum of the values show for phase.

The local variable phase contains one of these values and can be used inside an OnGraphDraw expression to detect which phase it is. When you select more than one of these options, you should usually include an If-Then branch on the local phase.

To access OnGraphDraw

By default, the OnGraphDraw attribute does not appear in the Attribute pane or Object window. To see it, go to the Attributes dialog on the Object menu and enable it for Variables.

It will now appear in the Attribute panel and Object window, where you can give it an Analytica expression.

Basics of drawing

To draw on the graph surface, use the Canvas drawing functions to draw to canv where canv is the name of a local variable available to you in the OnGraphDraw attribute. For example:

OnGraphDraw: CanvasDrawRectangle( canv, x:100, y:130, width:80, height:50, fillColor:0x440000ff )

OnGraphDraw rect1.png

This rectangle always appears at the same location, and doesn't adapt when the graph window is resized; therefore, its location does not necessarily coincide with any particular data, unless by chance. The difficult part is writing code that figures out where to draw. The plot area in canvas pixel coordinates is provided in the local variable named info as illustrated here:

OnGraphDraw: CanvasDrawEllipse(canv, x:info[OnGraphDrawItem='PlotAreaLeft'],
    y:info[OnGraphDrawItem='PlotAreaTop'],
    width: info[OnGraphDrawItem='PlotAreaWidth'],
    height: info[OnGraphDrawItem='PlotAreaHeight'],
    fillColor:0x440000ff )

OnGraphDraw plotArea.png

The y-axis scale goes from roles[GraphingRole='Y axis', GraphFillerInfo='Min'] to roles[GraphingRole='Y axis', GraphFillerInfo='Max']. So to annotate a threshold at y=1000, you could use this OnGraphDraw expression

OnGraphDraw afterFullyDrawn.png
Local yThresh := 1000;
Local (ignore,y) := GraphToCanvasCoord(info,roles,0,yThresh);
Local x1 := info[OnGraphDrawItem='PlotAreaLeft'];
Local x2 := info[OnGraphDrawItem='PlotAreaWidth'] + x1;
CanvasDrawLine( canv, x1,y, x2, y, color:'Green', width:3);
CanvasDrawText(canv, "Threshold", x1, y, color:'Green', vAlign:'Bottom')


OnGraphDraw threshold.png

Replacing the plot entirely, but using the same axes

A probability bands graph plot is normally draw with lines, such as:

OnGraphDraw OriginalBandsPlot.png

By using OnGraphDraw, we can retain the axes and axes scaling, but replace the plot itself with custom-drawn Tukey bars:

OnGraphDraw TukeyBands.png

In this case, the OnGraphDraw will be set as follows

OnGraphDraw TukeyBandsCall.png

Two key things are happening here. First, it is evaluated after the axes are drawn but before the data is drawn, and second, it sets the local continue to false if it draws the Tukey bars. The pre-existing UDF Tukey_bars returns true when it draws the bars, false otherwise. The function has a line that checks whether you are viewing the Bands view, so that other uncertainty views continue to render in their default fashion.

If info[OnGraphDrawItem='ViewMode']<>'Bands' Then ...

The key is also turned off in graph setup, since the colors shown in the key no longer apply.

Clipping to the plot area

When you draw to canv, you can draw on any part of the graph. So if you aren't careful, annotations that you expect to draw on the data may extend outside of the plot area (the area bounded by the axes). In some cases this may be what you want, but in other cases you may want to clip to the plot area.

In this example, error bars are draw around the mean value points without any clipping. It does not look natural for these bars to extend outside of the plot area.

OnGraphDraw:  { simplistic version }
  Local spread := SDeviation(Self);
  Local xPt := #roles[GraphingRole='X axis', GraphFillerInfo='Value'];
  Local yPt := #roles[GraphingRole='Y axis', GraphFillerInfo='Value'];
  Local (xUp,yUp) := GraphToCanvasCoord(info,roles,xPt,yPt+spread); 
  Local (xDn,yDn) := GraphToCanvasCoord(info,roles,xPt,yPt-spread);
  CanvasDrawLine(canv,xUp,yUp,xDn,yDn);
  CanvasDrawLine(canv,xUp-4,yUp,xUp+4,yUp);
  CanvasDrawLine(canv,xDn-4,yDn,xDn+4,yDn)
OnGraphDraw ErrorBarsNoClip.png

To fix this, you can draw to a canvas context that has been clipped to the plot area, rather than drawing directly to canv. The above code is modified as follows, where clip is a clipped canvas context:

OnGraphDraw:  { still simplistic, but with clipping }
  Local spread := SDeviation(Self);
  Local xPt := #roles[ GraphingRole='X axis', GraphFillerInfo='Value'];
  Local yPt := #roles[ GraphingRole='Y axis', GraphFillerInfo='Value'];
  Local (xUp,yUp) := GraphToCanvasCoord(info,roles,xPt,yPt+spread); 
  Local (xDn,yDn) := GraphToCanvasCoord(info,roles,xPt,yPt-spread);
  Local clip := Clip_to_PlotArea(canv, info);
  CanvasDrawLine(clip,xUp,yUp,xDn,yDn);
  CanvasDrawLine(clip,xUp-4,yUp,xUp+4,yUp);
  CanvasDrawLine(clip,xDn-4,yDn,xDn+4,yDn)
OnGraphDraw ErrorBarsWithClip.png

where the Clip_to_plotArea is a User-Defined Function that returns a canvas context clipped to the plot area. It is implemented as follows:

Function Clip_to_plotArea(canv,info) :=

Local (x,y,w,h) := _(...info[ OnGraphDrawItem='PlotArea'&['Left','Top','Width','Height'] ]);
CanvasContext(canv, clip:x,y,w,h)

Modifying the axis scale

The graphing engine selects the axis scale based on the data, and it does not know about stuff that you draw. In same cases you may need to adjust the axis scales so that the range includes your annotations. This section will illustrate one example where this comes up -- when drawing error bars around a point, the range of the error bars around mean-value points extend outside of the range of the plotted mean points. At least when the axis is auto-scaled, you need to extend the min and max data points. Another example occurs when rendering a Google map behind the data. You need to adjust the actual min and max latitude and longitude to the map actually returned by the Google server, even when your axes are manually scaled.

Changes to the scale must occur in the "before drawing" phase, so you need to enable evaluation of OnGraphDraw before drawing (as well as in the phase when you will actually render your annotations). During the before drawing phase, phase=2, your code needs to make modifications to the local variable roleChanges, which the rendering engine will then use while drawing the axes in the next phase.

The relevant addition for rescaling is highlighted. This part occurs during phase=2, whereas the remaining previous code is not in the else (not phase 2). The key point is that in phase=2, it modifies the min and max to include the extrema of the error bars.

OnGraphDraw PhaseBeforeAxes and after.png
Local spread := SDeviation(Self);
Local yRole := roles[ GraphingRole='Y axis' ];
Local yPt := #yRole[ GraphFillerInfo='Value' ];
If phase = 2 and yRole[ GraphFillerInfo='Autoscale'] Then (
     roleChanges[ GraphingRole='Y axis', GraphFillerInfo='Max' ] := MaxAll(yPt+spread);
     roleChanges[ GraphingRole='Y axis', GraphFillerInfo='Min' ] := MinAll(yPt-spread);
) Else (
     [[Local[[ xPt := #roles[ GraphingRole='X axis', GraphFillerInfo='Value' ];
     Local (xUp,yUp) := GraphToCanvasCoord(info,roles,xPt,yPt+spread); 
     Local (xDn,yDn) := GraphToCanvasCoord(info,roles,xPt,yPt-spread);
     Local clip := Clip_to_plotArea(canv,info);
     CanvasDrawLine(clip,xUp,yUp,xDn,yDn);
     CanvasDrawLine(clip,xUp-4,yUp,xUp+4,yUp);
     CanvasDrawLine(clip,xDn-4,yDn,xDn+4,yDn)
)
OnGraphDraw ErrorBarsWithAdjustedYScale.png

Modifying the series colors

(requires Analytica 6.0 or later)

In phase=2 (before drawing), you can change colors in the series colors used for the color sequence when an index is used for the color role. At present you can't use the method described here to modify the colors when a value is used for the color role. If there is an index assigned to the color role, then roles[GraphingRole='Color',GraphFillerInfo='ColorSequence'] is a reference to a 1-D array on that index and contains the colors that will be displayed. If the color role is unset or is set to a value, then this will be Null.

The following changes the color for J=4 to the color 0xF08080 (light coral) when J is used as the color key (Evaluate before drawing must be checked for OnGraphDraw).

Local colorSeq := roles[ GraphingRole='Color', GraphFillerInfo='ColorSequence'];
if Phase=2 and IsReference(colorSeq) and HasIndex(#colorSeq,J) Then
	roleChanges[ GraphingRole='Color',GraphFillerInfo='ColorSequence'] := 
		\(If J=4 Then 'LightCoral' Else colorSeq);

You can use either the color integer, 0xF08080, or the name 'LightCoral'.

This approach makes it possible to compute the color integer that would be used based on the data.

Adapting to Swap XY

The error bar example so far has a problem when you Swap XY. Can you spot it:

OnGraphDraw ErrorBarsSwapXY bad.png

The problem has to do with the way we draw the caps at the end of each error bar. They are still being drawn horizontally, but now need to be drawn vertically. Here the code that draws one of those ticks:

CanvasDrawLine(clip,xDn-4,yDn,xDn+4,yDn)

It draws a line from 4 pixels left of the bar end to 4 pixels right of the bar end. When we've swapped XY, we effectively want instead

CanvasDrawLine(clip,xDn,yDn-4,xDn,yDn+4)

The following addition accomplishes this (unchanged lines not shown):

Local ticW:=4, ticH:=0;
If info[ OnGraphDrawItem='SwapXY'] Then ( ticW:=0; ticH:=4 );
...
CanvasDrawLine(clip,xUp-ticW,yUp-ticH,xUp+ticW,yUp+ticH);
CanvasDrawLine(clip,xDn-ticW,yDn-ticH,xDn+ticW,yDn+ticH);

OnGraphDraw ErrorBarsSwapXY.png

Encapsulating drawing code

You may want to show error bars for a different variable. You don't want to repeat all this code every time, and of course, if you later make a bug fix or improvement, you'll want all your plots to inherit that fix. Hence, you should encapsulate this code as a User-Defined Function. Once you do this, when new variables want error bars, you'll just need to call your UDF from OnGraphDraw AND you'll need make sure to check the correct check boxes to specify when it is evaluated.

Encapsulating tends to be easy. You're function parameters are whichever of the special local variables (including possibly Self) that your code used. You have some choices about how you want to encapsulate it. For example, do you want your error bar function to only operate from Mean result view? Or do you want to let your caller decide? Do you want to always depict one standard deviation, or do you want your caller to specify the spread?

When your logic changes the continue or the roleChanges variable, then your function will need to return the new value, and the caller needs to set it. In the error bar example, it makes modifications to roleChanges, so this needs to be returned. So the call in OnGraphDraw will be:

roleChanges := Plot_error_bars( canv, info, roles, phase, SDeviation(Self) )

When you make changes to both, your function will need to return two return values, and your caller will use:

(continue, roleChanges) := My_annotation_function(....)

Wrapping it all up, the Plot_error_bars function becomes:

Function Plot_error_bars( canv, info, roles, phase, spread ) :=
Local yRole := roles[ GraphingRole='Y axis'];
Local yPt := #yRole[ GraphFillerInfo='Value'];
Local roleChanges := null;
 
If phase = 2 and yRole[ GraphFillerInfo='Autoscale' ] Then (
	roleChanges[ GraphingRole='Y axis', GraphFillerInfo='Max'] := MaxAll(yPt+spread);
	roleChanges[ GraphingRole='Y axis', GraphFillerInfo='Min'] := MinAll(yPt-spread);
) else if phase=8 then (
	Local ticW:=4, ticH:=0;
	If info[ OnGraphDrawItem='SwapXY'] Then ( ticW:=0; ticH:=4 );
	Local xPt := #roles[ GraphingRole='X axis', GraphFillerInfo='Value'];
	Local (xUp,yUp) := GraphToCanvasCoord(info,roles,xPt,yPt+spread); 
	Local (xDn,yDn) := GraphToCanvasCoord(info,roles,xPt,yPt-spread);
  	Local clip := Clip_to_plotArea(canv,info);
	CanvasDrawLine(clip,xUp,yUp,xDn,yDn);
	CanvasDrawLine(clip,xUp-ticW,yUp-ticH,xUp+ticW,yUp+ticH);
	CanvasDrawLine(clip,xDn-ticW,yDn-ticH,xDn+ticW,yDn+ticH);
);
roleChanges

See Also

Comments


You are not allowed to post comments.