Object subclass: #LogPlotter
    instanceVariableNames: 'name data form canvas '
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Ward-Plotting'


Draw as gif images Excel style charts of tabbed datasets read from text files.
(c) 2002, Ward Cunningham

Authors note: I wrote this code in a style I might use in Perl or Python. For example,
this script-like code reads and writes files within the scope of a web server and
cooperates with a cgi script. I found this lead me to make factoring decisions slightly
differently than if the objects were to become a permanent part of my Smalltalk world.
Even so, the coding style was effortless and should be considered a growth area for Squeak.

See SerializationThroughtput Additional Results.



LogPlotter methodsFor: 'input/output'

display
    form display

read
    | in out |
    in _ CrLfFileStream readOnlyFileNamed: name.
    out _ WriteStream on: Array new.
    [in atEnd] whileFalse:
        [out nextPut:
            (#(tab tab tab tab cr) collect:
                [:each | in upTo: (Character perform: each)])].
    data _ out contents
   

save
    GIFReadWriter
        putForm: form
        onFileNamed: self imageName


LogPlotter methodsFor: 'plotting'

axis
    | a b label |
    label _ self displayText: self runName.
    label align: label boundingBox bottomCenter with: self area topCenter.
    label displayOn: form.

    self xTics do:
        [:each |
        a _ self scale: (each @ self yTics first).
        b _ self scale: (each @ self yTics last).
        canvas line: a to: b width: 1 color: self gray.
        label _ self label: each.
        label align: label boundingBox topCenter with: a.
        label displayOn: form].

    self yTics do:
        [:each |
        a _ self scale: (self xTics first @ each).
        b _ self scale: (self xTics last @ each).
        canvas line: a to: b width: 1 color: self gray.
        label _ self label: each.
        label align: label boundingBox rightCenter with: -4@0 + a.
        label displayOn: form].

    form border: self area width: 1.


line: columnNumber color: aColor
    | xData yData to from dot transparentColor label |
    dot _ Form dotOfSize: 5.
    xData _ self sequence: 1.
    yData _ self sequence: columnNumber.
    transparentColor _ aColor alpha: 0.2.

    from _ nil.
    xData with: yData do:
        [:x :y |
        to _ self scale: x log @ y log.
        from notNil
            ifTrue: [canvas line: from to: to width: 3 color: transparentColor].
        form fillShape: dot fillColor: aColor at: to.
        from _ to.].

    label _ self displayText: (self title: columnNumber).
    to _ 2 @ columnNumber * label height + self area origin.
    canvas line: -10@0+to to: 10@0+to width: 3 color: transparentColor.
    form fillShape: dot fillColor: aColor at: to.
    label align: label boundingBox leftCenter with: 20@0 + to.
    label displayOn: form.

plot
    form _ Form extent: 640@480 depth: 16.
    canvas _ form getCanvas.
    canvas fillColor: Color white.
    self axis.
    self line: 3 color: Color red.
    self line: 4 color: Color green.
    self line: 5 color: Color brown.


LogPlotter methodsFor: 'support'

area
    ^form boundingBox insetBy:
        (55@20 corner: 20@20)

displayText: aString
    | displayText |
    displayText _ aString asDisplayText.
    displayText foregroundColor: Color gray backgroundColor: Color white.
    ^displayText

gray
    | gray |
    gray _ 0.8.
    ^Color r: gray g: gray b: gray

imageName
    ^(name allButLast: 3), 'gif'

label: anInteger
    | value |
    value _ (anInteger >= 0)
        ifTrue: [10 raisedToInteger: anInteger]
        ifFalse: [(10 raisedToInteger: anInteger) asFloat].   
    ^self displayText: value asString

runName
    | tokens |
    tokens _ (name findTokens: '\') last: 2.
    ^(tokens first, ' -- ', tokens last) allButLast: 4

scale: aPoint
    | scale |
    "linear to area"
    scale _ (self area width - 1 / (self xTics last - self xTics first )) @
        (self area height - 1 / (self yTics last - self yTics first)).
    ^(aPoint x - self xTics first * scale x + self area origin x) floor @
        (self area corner y - (aPoint y - self yTics first * scale y) - 1) floor

sequence: columnNumber
    ^data allButFirst collect:
        [:each | Number readFrom:
            (each at: columnNumber)]

setName: aString
    name _ aString.

title: columnNumber
    ^data first at: columnNumber

xTics
    ^0 to: 5

yTics
    ^-3 to: 7



LogPlotter class methodsFor: 'processing'

on: aString
    ^self new setName: aString

processDirectory: aString
    | aDirectory |
    "self processDirectory: 'C:\My Documents\Squeak\TestData'"
    "self processDirectory: '\\C2\ward\web\doc\SerializationThroughput\contrib\data'"
    aDirectory _ FileDirectory on: aString.
    (aDirectory fileNamesMatching: '*.txt') do:
        [:each | each ~= 'email.txt' ifTrue: [self processFile: (aDirectory fullNameFor: each)]].
    aDirectory directoryNames do:
        [:each | self processDirectory: (aDirectory fullNameFor: each)].
   

processFile: aString
    "self processFile: 'TestData\bk.txt'"
    (self on: aString)
        read;
        plot;
        display;
        save