Extensions to Plotchart

Arjen Markus (19 april 2010) I have been musing about extending Plotchart with a few more specialised chart types. The one I have realised so far could - a bit pretentiously - be called a SCADA display (cf. http://en.wikipedia.org/wiki/SCADA ). The code below has a few scars and bruises from the development process, but you will get the idea, I hope.


https://wiki.tcl-lang.org/_repo/images/plotscada.png


# plotscada.tcl --
#     Facilities for plotting "SCADA" displays:
#     - Schematic drawings of a factory for instance
#       and measured values in the various parts
#     - Telemetry systems
#

package require Tcl 8.5
package require Tk
package require Plotchart

namespace eval ::Plotchart {

   set methodProc(scada,scaling)           ScadaScaling
   set methodProc(scada,axis)              ScadaAxis
   set methodProc(scada,object)            ScadaObject
   set methodProc(scada,plot)              ScadaPlot
   set methodProc(scada,angular-scaling)   ScadaAngularScaling
}

# createScada --
#     Create a new command for plotting SCADA displays
#
# Arguments:
#    w             Name of the canvas
# Result:
#    Name of a new command
# Note:
#    The entire canvas will be dedicated to the display
#
proc ::Plotchart::createScada { w } {
    variable scada_scaling

    set newchart "scada_$w"
    interp alias {} $newchart {} ::Plotchart::PlotHandler scada $w
    #CopyConfig scada $w

    set pxmin 0
    set pxmax [expr {[WidthCanvas $w]  - 1}]
    set pymin [expr {[HeightCanvas $w] - 1}]
    set pymax 0

    # Default scaling: use pixels - not an error
    ScadaScaling $w default [list $pxmin $pymax $pxmax $pymin] \
                            [list $pxmin $pymin $pxmax $pymax]

    return $newchart
}


# ScadaScaling --
#     Create a new scaling for the SCADA display
#
# Arguments:
#    w             Name of the canvas
#    name          Name of the scaling
#    rectangle     Rectangle in the window (pixels)
#    worldcoords   Associated world coordinates
# Result:
#    None
# Note:
#    Order of the pixel coordinates: xmin, ymin, xmax, ymax
#    Ditto for the world coordinates, but the pixel y-coordinate
#    is actually inverted
#
proc ::Plotchart::ScadaScaling { w name rectangle worldcoords } {

    viewPort         $w,$name {*}$rectangle
    worldCoordinates $w,$name {*}$worldcoords

}


# ScadaObject --
#     Create a new object in the SCADA display
#
# Arguments:
#    w             Name of the canvas
#    type          Type of the object
#    objname       Name of the object
#    coords        List of coordinates
#    args          List of all remaining arguments
# Result:
#    None
# Side effects:
#    A new object is created on the canvas and its properties are stored
#
proc ::Plotchart::ScadaObject { w type objname coords args} {
    variable scada_scaling
    variable scada_object

    set scada_object($w,$objname,scaling) default

    set options {}
    foreach {key value} $args {
        switch -- $key {
            "-scaling" {
                set scada_object($w,$objname,scaling) $value
            }
            "-text" {
                set scada_object($w,$objname,text) $value
            }
            default {
                lappend options $key $value
            }
        }
    }

    switch -- $type {
        "rectangle" {
            set scada_object($w,$objname,canvasid) \
                [$w create rectangle {-10 -10 -10 -10} {*}$options]
        }
        "line" {
            set scada_object($w,$objname,canvasid) \
                [$w create line {-10 -10 -10 -10} {*}$options]
        }
        "polygon" {
            set scada_object($w,$objname,canvasid) \
                [$w create polygon {-10 -10 -10 -10 -10 -10} {*}$options]
        }
        "text" {
            set scada_object($w,$objname,canvasid) \
                [$w create text {-10 -10} -text $scada_object($w,$objname,text) {*}$options]
        }
        default {
            return -code error "Unknown object type: $type"
        }
    }

    set scada_object($w,$objname,coords) $coords
    set scada_object($w,$objname,type)   $type
}


# ScadaPlot --
#     Change the properties (coordinates or text) of a SCADA object
#
# Arguments:
#    w             Name of the canvas
#    objname       Name of the object
#    args          List of the parameters that need to be changed
# Result:
#    None
# Side effects:
#    The coordinates or the text are changed
#
proc ::Plotchart::ScadaPlot { w objname args} {
    variable scada_scaling
    variable scada_object

    set wcoords $scada_object($w,$objname,coords)

    set replace 0
    set index   0
    foreach coord $wcoords {
        if { $coord eq "*" } {
            lset wcoords $index [lindex $args $replace]
            incr replace
        }
        incr index
    }

    set scaling $w,$scada_object($w,$objname,scaling)

    set coords {}
    foreach {x y} $wcoords {
        if {[catch {
        foreach {px py} [coordsToPixel $scaling $x $y] {break}
        lappend coords $px $py
        } msg] } {
            puts "problem: $scaling -- $x -- $y"
        }
    }

    $w coords $scada_object($w,$objname,canvasid) $coords

    if { $scada_object($w,$objname,type) eq "text" } {
        if { $scada_object($w,$objname,text) eq "*" } {
            set text [lindex $args end]
            $w itemconfig $scada_object($w,$objname,canvasid) -text $text
        }
    }
}

# main --
#     Simple test:
#     - Display a stirred vessel
#     - Flow rate of two components can be regulated via the scale widgets
#     - The reaction in the vessel is modelled with the "chemical" equation:
#
#           A + B --> C + heat
#
#     - Nothing realistic, just playing around with the possibilities
#
# Display a simple vessel
#
pack [canvas .c -height 300 -width 600]

set p [::Plotchart::createScada .c]

scale .c.scalea -orient vertical -from 0.10 -to 0.0 -variable ratea -tickinterval 0.02 -digits 3 -resolution 0.001
scale .c.scaleb -orient vertical -from 0.10 -to 0.0 -variable rateb -tickinterval 0.02 -digits 3 -resolution 0.001
button .c.start -text Start -command {startComputation} -width 10
button .c.stop  -text Stop  -command {stopComputation}  -width 10

.c create window  10  30 -window .c.scalea -anchor nw
.c create window 110  70 -window .c.scaleb -anchor nw
.c create window  10 200 -window .c.start  -anchor nw
.c create window 110 200 -window .c.stop   -anchor nw

.c create line { 20  40  230   40} -width 2 -arrow last -fill red
.c create text  230  30  -text "Component A" -anchor e  -fill red
.c create line {200  70  230   70} -width 2 -arrow last -fill blue
.c create text  230  60  -text "Component B" -anchor e  -fill blue
.c create line {325 210  360  210} -width 2 -arrow last
.c create text  340 200  -text "Product C" -anchor w
.c create polygon {240  10 240  10
                   240 240 240 240
                   280 260
                   320 240 320 240
                   320  10 320  10} \
     -fill orange -smooth 1 -outline black -width 2

.c create line      {280   0 280 200} -width 2
.c create line      {270 200 290 200} -width 2
.c create rectangle {250 190 270 210} -width 2 -fill black
.c create rectangle {290 190 310 210} -width 2 -fill black


$p scaling temperature {400  22 428  148} {0.0 10.0 1.0 100.0}
.c create rectangle {400 20 430 150} -width 2 -fill white -outline black

$p object rectangle thermometer {0.0 10.0 1.0 *} -scaling temperature -fill red
$p object text temperature {1.3 *} -text * -scaling temperature -anchor w

$p scaling concentration {480 102 510 248} {0.0 0.0 1.0 1.0}
.c create rectangle {480 100 510 250} -width 2 -fill yellow -outline black

$p object rectangle conca   { 0.0 0.0  1.0 *} -fill red    -scaling concentration
$p object rectangle concb   { 0.0   *  1.0 *} -fill blue   -scaling concentration

$p object text      labela  {1.3 *} -text A      -scaling concentration
$p object text      labelb  {1.3 *} -text B      -scaling concentration
$p object text      labelc  {1.3 *} -text C      -scaling concentration

$p object line      maxline {-0.1   *  1.4 *}              -scaling concentration
$p object text      maximum { 1.4   *}       -text Maximum -scaling concentration -anchor w

$p plot maxline 0.25 0.25
$p plot maximum 0.25
$p plot labelc  1.0

$p plot thermometer 25.0
$p plot temperature 25.0 "25"

$p plot conca  0.1
$p plot concb  0.1 0.3
$p plot labela 0.1
$p plot labelb 0.3

#
# Computational and GUI control part
#

# nextStep --
#     Compute the values at the next step
#
# Arguments:
#     time           Current time (not used)
#     values         Current values
#     deltt          Time step
#
# Result:
#     Values at the new time level
#
proc nextStep {time values deltt} {
    global k h alpha qa qb conca0 concb0 rcp temp0

    foreach {conca concb concc temp} $values {break}

    set rateab [expr {$k * exp( $alpha * $temp ) * $conca * $concb}]

    set dva    [expr {-$rateab + $qa * $conca0 - ($qa+$qb) * $conca}]
    set dvb    [expr {-$rateab + $qb * $concb0 - ($qa+$qb) * $concb}]
    set dvc    [expr {$rateab - ($qa+$qb) * $concc}]
    set dvt    [expr {($h * $rateab + ($qa+$qb) * ($temp0 - $temp))/$rcp}]

    return [list \
        [expr {$conca + $dva * $deltt}] \
        [expr {$concb + $dvb * $deltt}] \
        [expr {$concc + $dvc * $deltt}] \
        [expr {$temp  + $dvt * $deltt}]]
}

#
# Set the coefficients
#

set k       1.0e-2
set alpha   0.1
set ratea   0.02
set rateb   0.02
set conca0  10
set concb0  10
set temp0   20
set h       3.0e5
set rcp     4.2e6

set conca   0.0
set concb   0.0
set concc   0.0
set temp    20.0

set time    0.0
set deltt   0.001

set values  [list $conca $concb $concc $temp]


proc startComputation {} {
    set ::stop 0

    set ::qa $::ratea
    set ::qb $::rateb

    nextTime $::p $::time $::values $::deltt
}

proc stopComputation {} {
    set ::stop 1
}


proc nextTime {display time values deltt} {
    if { $::stop } {
        return
    }

    foreach {conca concb concc temp} $values {break}

    $display plot thermometer $temp
    $display plot temperature $temp [format %5.1f $temp]

    set total [expr {$conca+$concb+$concc}]
    if { $total != 0.0 } {
        $display plot conca [expr {$conca/$total}]
        $display plot concb [expr {$conca/$total}] [expr {($conca+$concb)/$total}]
    }

    if { $temp > 100.0 } {
        tk_messageBox -type ok -message "Temperature is over 100 degrees!\nHalting computation"
        return
    }

    for {set i 0} {$i < 10000} {incr i} {
        set time [expr {$time + $deltt}]
        set values [nextStep $time $values $deltt]
    }

    after 1 [list nextTime $display $time $values $deltt]
}