Rules for Drawing Graph Elements, Especially Axes

Since I don’t have time to work through much code today I thought I’d take a step back and define the rules I’ve implemented–or will implement–that govern the construction of the different elements of the graph object I’ve been developing. The process of adding new capabilities always illustrates assumptions you’ve made in previous code and where you want to be able to break things up to make it more modular and flexible. Nowhere is this more important than in the construction of the axis elements and their associated labels.

The basic outline of the process so far is to determine as much information about the size of elements as possible so they can be placed onto the canvas with the correct amount of room. I’ve done this from the outer edges of the canvas working in, so an important intermediate result is the determination of the area available to draw the actual plot(s) (i.e., the central area where the actual data is shown graphically).

By default, I’ve assumed that the entire canvas will be taken up by the graph object’s components but as I think about it, it might be a good idea to make the outer boundaries completely arbitrary. I can think of a number of reasons for this, one of which would be the ability to place a stripped-down, real-time scrolling chart at a user-specified location on a larger animation of some process being monitored (and/or simulated). Could you place another DOM element (canvas) over an existing display? Sure, but being able to simply save an area and plot a graph in an arbitrary location on a larger canvas probably gives more flexibility. This generates two more changes to all the code so far written: 1) the x- and y-coordinates of each edge have to be specified for the graph object’s area, rather than assuming that the coordinates correspond to the edge of the host canvas, and 2) all internal locations have to be calculated from an offset x and y origin, rather than assuming a default origin of 0,0. That change alone will touch a good proportion of the code so far written.

I have to account for a few things I haven’t accounted for yet, all of which items were already included in my earlier To Do list. These include legends, labels with line wraps, axes on either end of the plotting area, the inclusion of multiple x-axes or multiple y-axes, and the ability to reverse the direction of any axis.

The big news from my most recent work is defining rules for how axes are defined and drawn. It was obviously easier to implement linear axes first, but the process of adding the ability to draw axes with an exponential scale proved illuminating. The initial set of rules I came up with for linear scales were the following:

  • I defined the end points of each axis as coordinates in pixels, using an x,y pair for each end of the axis.
  • I assumed that any axis must be vertical or horizontal. In theory this would save me from having to specify one of the four coordinate values (e.g., the y-values for both ends of the x-axis would be the same) but chose not to do that for clarity.
  • I defined separate pairs of inner and outer high and low values for each axis. The inner high and low values are intended to reflect the highest and lowest values actually included in the data or plot (when these can be determined automatically and when it isn’t acceptable to allow plot points outside of the graph bounds). The outer high and low values are intended to coincide with the beginning and end of the axis. The theory there is that there should or at least could be buffer space at either end of the data set. When the inner range can be known the outer range could then be determined automatically in a way that would allow for a reasonably sized spatial buffer. The automated parts of this mechanism have not yet been implemented. So far the ranges have been set manually.
  • Each axis has been divided by an integer number of major ticks, not counting the major tick defined to fall at the start of the axis. That is, if the number of major ticks is defined as four, then there will be five ticks defining four (evenly spaced) intervals along the axis. So far I’ve chosen values for the outer range of values and the number of ticks that generally yield even values at each major tick mark, and I’ve done this manually. It is also possible to define and origin value and incremental values and let the number and location of tick marks and values be determined from that. In that case the final tick might not coincide with the far end of the axis; the number of cycles could end in a fraction. I’ve had it in mind to support axis labels to begin and end at arbitrary values, but have not implemented any such support.
  • Each increment of major tick values can be further subdivided by a specified number of minor ticks. Choosing zero or a positive number of minor ticks defines that number of intervals plus one. The minor ticks are then spaced equally down the length of the axis, between each pair of major ticks. The original implementation calculated the total number of tick marks (major and minor) as major * (minor + 1), and then the ticks were all drawn in order. After implementing the logarithmic scale drawing process I see it would be more consistent to draw the minor ticks as a sub-process of drawing each major tick. That would allow the relevant pieces of code to be constructed in a more consistent manner and probably allow greater flexibility in dealing with partial major tick intervals.
  • Tick value labels are only drawn in association with major ticks. (Note that the ticks do not actually have to be drawn, and the graph area lines may similarly be drawn or not drawn.)
  • The number of pixels the first and last major tick values take up beyond the location of the tick itself are calculated for each axis, in the direction of that axis. The number of pixels perpendicular to the axis is also calculated for the longest label. This information may be required to ensure that proper buffer space is provided so all such values will be displayed in full. If either of the end labels do not fall at exactly at the end of the axis, then further adjustments would have to be made. I have not implemented calculation that deal with any aspect of partial ranges for axes with linear scales.
  • The tick value labels are placed so the center line of the text (running along the length of the text evenly between the top and bottom) intersects the end of the tick plus an offset defined by a parameter. So far the spacing from top to bottom has not considered the height of any possible descenders, because only numeric labels have been supported so far. The code may be modified to consider the presence of descenders on such labels in the futures.
  • The dimensions of text label for each axis have a bearing on the spacing required to display all elements. So far only the height of the characters is considered along with a buffer between the label and the outer edge of the tick value labels (perpendicular to the associated axis). Provision for multi-line text labels has not yet been implemented. Provision for inserting line wraps for very long text labels (or possible adjustment of the wrap location by the user) based on the amount of space available (parallel to the associated axis), has not yet been implemented. Flags governing whether or not to display text label for each axis have been implemented, but not all functionality associated with drawing or not drawing the label and adjusting the space required for the elements has been implemented. So far I have ensured that text labels are always defined and drawn.

Many of the rules for drawing axes with logarithmic scales are the same but the necessary differences are these:

  • The lower and upper bounds of the range are defined manually.
  • The major intervals are defined by a multiplier which is successively applied. The multiplier is referred to as the Base. For example, if the lower bound is set as 1.0, the following major interval values would be 1.0, 5.0, 125.0, 625.0, 3125.0, and so on. A lower bound of 3.3 with a base of 10 would yield 3.3, 33, 330, 3300, and so on. A lower bound of 1.0 with a base of 4.5 would yield 1.0, 4.5, 20.25, 91.125, 410.0625, and so on. The lower bound and base can be any value and do not have to be whole numbers. The lower bound will always define the location of a major tick.
  • The upper bound may fall anywhere in a major interval. The calculations for how this affects the overflow pixels beyond the far end of the axis have been implemented in this case, though the example I’ve actually plotted ends on an even boundary.
  • The number of minor ticks is determined by the base value minus two, rounded down. That is, a base of 10 would yield 8 minor ticks (2-9), a base of 5.7 would yield 3 (2-4), a base of 3 would yield 1, and a base of 2 would yield none.
  • The number of cycles is given by the logarithm of the maximum value divided by the minimum value, which value is then divided by the log of the base value. For example, a range of 0.018 to 1800 yields a multiple of 100,000, or 10 to power of 5. The natural log of 100,000 (11.5129) divided by the natural log of 10 (2.30258) also works out to 5. This math works for any base value.
  • Once the pixel length of the axis is determined the number of pixels per cycle can be determined.
  • Once the pixels per cycle is determined the location of each minor tick within a cycle can be determined in pixels. The fraction of the cycle length for each tick is given by the natural log of the tick number plus 1 (counting from 1 to the number of ticks) divided by the natural log of the base value. When each major tick is drawn the information then exists to be able to draw all of the minor ticks at one (as a sub-process of drawing the major ticks as described above). Care must be taken to ensure minor ticks are not drawn past then end of the axis for partial cycles, which code has not yet been implemented.
  • Plotting values on a log scale turns out to be more difficult than for a linear scale. Start by taking the natural log of the quotient of the value to be plotted divided by the minimum value, then divide that by the natural log of the base value. Then divide that by the number of cycles. Then you can apply that fraction to the pixel length of the axis. (Note that I still have a small issue to work out about how to count the end pixel.)
This entry was posted in Tools and methods and tagged , . Bookmark the permalink.

Leave a Reply