timeliner

Richard Suchenwirth 2004-08-10 - Yet another thing to do with a canvas: history visualisation of a horizontal time-line, for which a year scale is displayed on top. The following kinds of objects are so far available:

  • "eras", displayed in yellow below the timeline in boxes
  • "background items" that are grey and stretch over all the canvas in height
  • normal items, which get displayed as stacked orange bars

WikiDbImage timeliner.jpg

You can zoom in with <1>, out with <3> (both only in x direction). On mouse motion, the current year is displayed in the toplevel's title. Normal items can be a single year, like the Columbus example, or a range of years, for instance for lifetimes of persons. (The example shows that Mozart didn't live long...)


 namespace eval timeliner {
    variable ""
    array set "" {-zoom 1  -from 0 -to 2000}
 }
 proc timeliner::create {w args} {
    variable ""
    array set "" $args
    #-- draw time scale
    for {set x [expr ($(-from)/50)*50]} {$x<=$(-to)} {incr x 10} {
        if {$x%50 == 0} {
            $w create line $x 8 $x 0
            $w create text $x 8 -text $x -anchor n
        } else {
            $w create line $x 5 $x 0
        }
    }
 #  bind $w <Motion> {wm title . [expr int([%W canvasx %x]/$::timeliner::(-zoom))]}
    bind $w <Motion> {timeliner::title %W %x ; timeliner::movehair %W %x}
    bind $w <1> {timeliner::zoom %W %x 1.25}
    bind $w <2> {timeliner::hair %W %x}
    bind $w <3> {timeliner::zoom %W %x 0.8}
 }
 proc timeliner::movehair {w x} {
    variable ""
    if {[llength [$w find withtag hair]]} {
        set x [$w canvasx $x]
        $w move hair [expr {$x - $(x)}] 0
        set (x) $x
    }
 }
 proc timeliner::hair {w x} {
    variable ""
    if {[llength [$w find withtag hair]]} {
        $w delete hair
    } else {
        set (x) [$w canvasx $x]
        $w create line $(x) 0 $(x) [$w cget -height] \
                  -tags hair -width 1 -fill red
    }
 }
 proc timeliner::title {w x} {
    variable ""
    wm title . [expr int([$w canvasx $x]/$(-zoom))]
 }
 proc timeliner::zoom {w x factor} {
    variable ""
    $w scale all 0 0 $factor 1
    set (-zoom) [expr {$(-zoom)*$factor}]
    $w config -scrollregion [$w bbox all]
    if {[llength [$w find withtag hair]]} {
        $w delete hair
        set (x) [$w canvasx $x]
        $w create line $(x) 0 $(x) [$w cget -height] \
                  -tags hair -width 1 -fill red
    }
 }

This command adds an object to the canvas. The code for "item" took me some effort, as it had to locate a free "slot" on the canvas, searching top-down:

 proc timeliner::add {w type name time args} {
    variable ""
    regexp {(\d+)(-(\d+))?} $time -> from - to
    if {$to eq ""} {set to $from}
    set x0 [expr {$from*$(-zoom)}]
    set x1 [expr {$to*$(-zoom)}]
    switch -- $type {
        era    {set fill yellow; set outline black; set y0 20; set y1 40}
        bgitem {set fill gray; set outline {}; set y0 40; set y1 1024}
        item   {
            set fill orange
            set outline yellow
            for {set y0 60} {$y0<400} {incr y0 20} {
                set y1 [expr {$y0+18}]
                if {[$w find overlap [expr $x0-5] $y0 $x1 $y1] eq ""} break
            }
        }
    }
    set id [$w create rect $x0 $y0 $x1 $y1 -fill $fill -outline $outline]
    if {$type eq "bgitem"} {$w lower $id}    
    set tid [$w create text [expr {$x0+5}] [expr {$y0+2}] -text $name -anchor nw]
    foreach arg $args {
        if {$arg eq "!"} {
            # modified by buchs $w itemconfig $tid -font "[$w itemcget $tid -font] bold"
            # appropriately handles return of TkDefaultFont
            set fontused [font actual [$w itemcget $tid -font]]
            set fontbold [regsub -- {-weight [^ ]*} $fontused {-weight bold}]
            $w itemconfig $tid -font "$fontbold"
        }
    }
    $w config -scrollregion [$w bbox all]
 }

Here's a sample application, featuring a concise history of music in terms of composers:

 scrollbar .x -ori hori -command {.c xview}
 pack      .x -side bottom -fill x
 canvas    .c -bg white -width 600 -height 300 -xscrollcommand {.x set}
 pack      .c -fill both -expand 1
 timeliner::create .c -from 1400 -to 2000

These nifty shorthands for adding items make data specification a breeze - compare the original call, and the shorthand:

    timeliner::add .c item Purcell 1659-1695
    - Purcell 1659-1695

With an additional "!" argument you can make the text of an item bold:

 foreach {shorthand type} {* era  x bgitem - item} {
    interp alias {} $shorthand {} timeliner::add .c $type
 }
#-- Now for the data to display (written pretty readably):
 * {Middle Ages} 1400-1450
 - Dufay 1400-1474
 * Renaissance    1450-1600
 - Desprez 1440-1521
 - Luther 1483-1546
 - {Columbus discovers America} 1492
 - Palestrina 1525-1594 !
 - Lasso 1532-1594
 - Byrd 1543-1623
 * Baroque        1600-1750
 - Dowland 1563-1626
 - Monteverdi 1567-1643
 - Schütz 1585-1672
 - Purcell 1659-1695
 - Telemann 1681-1767
 - Rameau 1683-1764
 - Bach,J.S. 1685-1750 !
 - Händel 1685-1759
 x {30-years war} 1618-1648
 * {Classic era}  1750-1810
 - Haydn 1732-1809 !
 - Boccherini 1743-1805
 - Mozart 1756-1791 !
 - Beethoven 1770-1828 !
 * {Romantic era} 1810-1914
 - {Mendelssohn Bartholdy} 1809-1847
 - Chopin 1810-1849
 - Liszt 1811-1886
 - Verdi 1813-1901
 x {French revolution} 1789-1800
 * {Modern era}   1914-2000
 - Ravel 1875-1937 !
 - Bartók 1881-1945
 - Stravinskij 1882-1971
 - Varèse 1883-1965
 - Prokof'ev 1891-1953
 - Milhaud 1892-1974
 - Honegger 1892-1955
 - Hindemith 1895-1963
 - Britten 1913-1976
 x WW1 1914-1918
 x WW2 1938-1945

 bind . <Escape> {exec wish $argv0 &; exit}
 bind . <F1> {console show}

US Nice. I took the freedom to add a red hair. Toggle on/off with middle button.

escargo - Note that not all user interfaces will provide three mouse buttons, like the laptop I'm using right now.

RS: Thanks for your interest and comments. Testing the hair, I find that my mouse has a middle button, but Tk seems not to respond to it :( I noted however that someone changed references to the "anonymous array" "" into the namespace path ::timeliner:: . That is redundant and less convenient, should the name of the namespace change. So I changed back references like

   $::timeliner::(-zoom)

to the equivalent (as long as we're in the ::timeliner namespace)

   $(-zoom)

That's why I declare variable "", and that's what the anonymous array was created for - beautiful minimal syntax ... :)


Sep-4-2004 JM If you want to map events in minutes (like activities in a working day) you can see this very same code, with just a few lines modified timeliner (minutes instead of years)


AMG: Somebody munged the encoding, so I fixed it back using [encoding convertfrom utf-8]. Please be careful when editing pages.


Jan-28-2009 buchs: I added some code to the bolding section, to enable handling a return of TkDefaultFont.