Version 8 of Chart generation support

Updated 2003-08-02 10:57:20

Dave Griffin (1-Aug-2003) writes:

I recently needed to dynamically generate some graphics for inclusion on a web page. There are a couple of techniques for doing this with various tradeoffs:

  • HTML-based (div/gif magic) charts are quick and dirty, but are difficult to make look professional.
  • BLT is an obvious choice, and combined with Img you can snag nice graphs and convert them to JPEG or GIF, but the application must be able to map the window briefly to grab the pixels and this is less than acceptable on a server. Generating postscript from BLT and rendering it via Ghostscript is an option, but introduces packaging issues and JPEG is not the ideal graphics file format for charts and graphs -- too many artifacts. Changing BLT to generate SVG is definitely an option, but not possible in the timeframe we have.
  • Generating charts with packages like tcl-magick are a good option, but required compiled extensions. This is our fallback option right now.
  • Given the above we have decided to try to use SVG as the rendering engine on the browser and to generate the graphs using the canvas widget and the handy can2svg routines. All this will be wrapped up in a Starkit which will be running as a daemon with the root window withdrawn. We'll see.

Anyway, because we can't reuse all of the great stuff in BLT, there is a need to build some of the basic tools. There are many examples of drawing line and bar charts in this Wiki (Simple data plotting example, Shuffle a list: graph results), and at one level it is pretty simple to produce a custom graphic. When you get into the more general-purpose stuff, it gets a bit harder (as I'm sure George Howlett could attest).

As I build this, I'm going to keep track of some of the support routines I'm developing to create the charting package. I hope that I do this in a way that you can use other examples here in the Wiki and combine them with these routines to create better charts and graphs. Comments and improvements welcome (neither will be hard to come by).

    namespace eval chart {}

    #
    # nice_number
    #
    #   Reference: Paul Heckbert, "Nice Numbers for Graph Labels",
    #          Graphics Gems, pp 61-63.  
    #
    #   Finds a "nice" number approximately equal to x.
    #
    #   Args: x -- target number
    #         round -- If non-zero, round. Otherwise take ceiling of value.

    proc chart::nice_number {x round} {

        #   expt -- Exponent of x 
        #   frac -- Fractional part of x 
        #   nice -- Nice, rounded fraction 

        set expt [expr floor(log10($x))]
        set frac [expr $x / pow(10.0, double($expt))]
        if ($round) {
            if {$frac < 1.5} {
                set nice 1.0
            } elseif {$frac < 3.0} {
                set nice 2.0
            } elseif {$frac < 7.0} {
                set nice 5.0
            } else {
                set nice 10.0
            }
        } else {
            if {$frac <= 1.0} {
                set nice  1.0
            } elseif {$frac <= 2.0} {
                set nice  2.0
            } elseif {$frac <= 5.0} {
                set nice 5.0
            } else {
                set nice 10.0
            }
        }
        return [expr $nice * pow(10.0, double($expt))]
    }




    #
    # loose_label
    #
    #   Reference: Paul Heckbert, "Nice Numbers for Graph Labels",
    #          Graphics Gems
    #
    #   Returns a set of graph labelling attributes given a range of numbers
    #   that need to be graphed.
    #
    #   Args: min   -- Lower range boundary
    #         max   -- Upper range boundary
    #         steps -- Number of major tick marks desired
    #

    proc chart::loose_label {min max {steps 5}} {
        if {$steps < 2} { set steps 2 }
        # We expect min!=max 
        # Try and be nice by raising max by 1.  This obviously
        # fails miserably for small values.
        if {$min == $max} { set max [expr $max + 1] }

        set range [nice_number [expr $max - $min] 0]
        # tick mark spacing
        set d [nice_number [expr $range / ($steps - 1)] 1]
        set graphmin [expr floor($min/$d) * $d]
        set graphmax [expr ceil($max/$d) * $d]
        set nfx [expr int(-floor(log10($d)))]
        set nfrac [expr ($nfx > 0) ? $nfx : 0]
        set stepfmt [format "%%.%df" $nfrac]

        set ticks [list]
        for {set x $graphmin} {$x < [expr $graphmax + 0.5 * $d]} {set x [expr $x + $d]} {
            lappend ticks $x
        }

        return [list graphmin $graphmin graphmax $graphmax step $d stepfmt $stepfmt ticks $ticks]
    }


    #
    # range_and_step
    #
    # Given a range (and a maximum number of steps), returns a 
    # "nice" range and "nice" step (tick) size
    #

    proc chart::range_and_step { range {steps 5}} {
        if {$steps < 2} { set steps 2 }
        set range [nice_number $range 0]
        set step  [nice_number [expr $range / ($steps - 1)] 1]
        return [list $range $step]
    }

Here's a sample of how the results of the chart routines can be used to draw a chart axis:

    proc draw_y_axis { canvas x y height axisProps {fgcolor black} } {

        array set ap $axisProps
        set y1 [expr $y - $height]
        $canvas create line $x $y $x $y1
        set nTicks [llength $ap(ticks)]
        set tickIncr [expr $height / $nTicks]
        set ty $y
        foreach t $ap(ticks) {
            $canvas create line $x $ty [expr $x - 5] $ty
            $canvas create text [expr $x - 7] $ty -justify right -anchor e -text [format $ap(stepfmt) $t]
            set ty [expr $ty - $tickIncr]
        }
    }


    # A non-functioning example (not a good Wiki code example yet)
    set yap [chart::loose_label 0 $maxRange]
    draw_y_axis .c $x_origin $y_origin 100 $yap

If these prove to be of some value and are stable, they may be useful additions to the TclLib math::statistics package, which has some basic plotting tools in it.

Sounds to me like the items on this page would be great additions to tklib.


See also A little function plotter

Category Application | Category Graphics