REBOL [
Title: "Quick Plot Dialect"
Date: 15-Feb-2002
Version: 0.1.0
File: %q-plot.r
Author: "Matt Licholai"
Rights: "(c) 2001,2002 M. Licholai"
Usage: {Use as a block feeding REBOL VID dialect. See ez-plot.r for details}
Purpose: {Provide a quick and easy to use dialect for plotting in REBOL}
Comment: {To-Do ==
High --
Med --
^-^- + Update/Expand ez-plot.r documentation
Low --
^-^- + Add stacked bar charts
^-^- * Rework x log scaling
}
History: [0.0.1 [10-Dec-2001 "Initial dialect based on my plot functions"]
0.0.2 [14-Dec-2001 "Basic aspects working."]
0.0.3 [17-Dec-2001 {Changed to return a layout object, making display easier.}]
0.0.4 [5-Jan-2002 "Cleaned up code and comments, added line-patterns"]
0.0.5 [15-Jan-2002 {Corrected a problem with offsetting the
x start point and not centering bars}]
0.0.6 [16-Jan-2002 {Corrected problem with labels and various bug fixes}]
0.0.7 [19-Jan-2002 "Added multi-plot function"]
0.0.8 [21-Jan-2002 "Minor clean-up and removal of global words"]
0.0.9 [29-Jan-2002 {Start Change to an object, improve parse block,
add log scales and candlestick stock chart}]
0.1.0 [15-Feb-2002 {Completed changes to parse, and added basic pie chart}]
0.1.1 [16-Feb-2002 {Cleaned up code/comments and added exploded pie charts}]
]
Email: m.s.licholai@ieee.org
library: [
level: [intermediate]
platform: [all]
type: [dialect]
domain: [graphics dialects math gui]
tested-under: none
support: none
license: none
see-also: %ez-plot.r
]
]
quick-plot-dialect: make object! [
CVS-Id: { $Id: q-plot.r,v 1.33 2002/02/19 15:30:18 matt Exp $ }
; ------------------
; "declare" words that will be used in the object
; This is needed to prevent words in the object from
; bubbling up to global level (Basically the same as 'use
; but doesn't introduce a new level of indentation.)
; This is needed due to a bug? in the way REBOL creates binding
; for word inside functions within objects.
; Set these key words in the object to none
; --------------------
set-y-min: none
set-y-max: none
set-x-min: none
set-x-max: none
title: none
title-vals: none
label-nr: none
bar-width: none
y-data: none
x-data: none
p-size: none
full-size: none
result: none
obj-parms: none
default-font: none
default-color: none
back-color: none
default-fill: none
up-color: none
down-color: none
border-width: none
x-border: none
y-border: none
x-log-scale: none
y-log-scale: none
title-style: none
dynamic-scale: none
dyn-pct: none
; ---------------------------------------------
; Initialize and re initialize when needed
; ---------------------------------------------
initialize: func [
[catch]
/local
x-min x-max y-min y-max y-diff y-scaler y-scaled-zero x-step bar-step temp-blk
][
if (obj-parms) [return obj-parms] ; skip the initialization code if we've already done it
; -------------------------------------------------------
; Next we ensure that min and max y and x values are set
; properly based on the type of data we are initially given.
; Do not over ride a user supplied value.
; -------------------------------------------------------
either set-y-min [y-min: set-y-min] [
either y-data [
y-min: first minimum-of y-data
if none? y-min [
temp-blk: sort copy y-data
while [none? y-min] [
y-min: first temp-blk: next temp-blk
]
]
][
y-min: first minimum-of third stock-data
if none? y-min [
temp-blk: sort copy y-data
while [none? y-min] [
y-min: first temp-blk: next temp-blk
]
]
]
]
either set-y-max [y-max: set-y-max] [
either y-data [
y-max: first maximum-of y-data
if none? y-max [
temp-blk: sort copy y-data
while [none? y-max] [
y-max: first temp-blk: next temp-blk
]
]
][
y-max: first maximum-of second stock-data
if none? y-max [
temp-blk: sort copy y-data
while [none? y-max] [
y-max: first temp-blk: next temp-blk
]
]
]
]
either set-x-min [x-min: set-x-min] [
either x-data [
x-min: first minimum-of x-data
if none? x-min [
temp-blk: sort copy x-data
while [none? x-min] [
x-min: first temp-blk: next temp-blk
]
]
][
x-min: 1
]
]
either set-x-max [x-max: set-x-max] [
either x-data [
x-max: first maximum-of x-data
if none? x-max [
temp-blk: sort copy x-data
while [none? x-max] [
x-max: first temp-blk: next temp-blk
]
]
][
either y-data [
x-max: length? y-data
][
x-max: length? fourth stock-data
]
]
]
; if give a block of stock prices, work with the closes
if not y-data [y-data: fourth stock-data]
if dynamic-scale [
either ((abs y-max - y-min / y-max * 100) > dyn-pct) [
y-log-scale: true
][
y-log-scale: false
]
]
y-diff: to-decimal either y-log-scale [
either any [ y-max = 0 y-min = 0] [
log-10 (y-max - y-min)
][
either any [(y-min >= 0.0) (y-max <= 0.0)] [
; min and max are both have the same sign
abs ((log-10 abs y-max) - log-10 (abs y-min))
][
; min and max cross zero
(log-10 y-max) + log-10 abs y-min
]
]
][
y-max - y-min
]
y-scaler: (to-decimal p-size/y) / y-diff
; start computing the y-scaled-zero
either y-log-scale [
either y-min = 0.0 [y-scaled-zero: 0.0][
y-scaled-zero: y-scaler * log-10 (abs y-min)
]
if y-min < 0 [
y-scaled-zero: negate y-scaled-zero
if y-max > 0 [y-scaled-zero: (negate y-max / (y-max + y-max) * p-size/y)]
]
][
y-scaled-zero: (y-scaler * y-min)
]
; is there a clear way to show what's being done here?
y-scaled-zero: p-size/y + y-scaled-zero
x-offset: either x-border [border-width] [0]
x-step: (to-decimal p-size/x) / (to-decimal (length? y-data) - 1 )
x-log-adj: either x-log-scale [
(to-decimal length? y-data) / (0.25 + length? y-data)
][
1.0
]
bar-step: (to-decimal p-size/x) / (to-decimal (length? y-data ))
obj-parms: reduce ['y-min y-min 'y-max y-max 'x-min x-min 'x-max x-max
'y-scaler y-scaler 'y-scaled-zero y-scaled-zero 'x-step x-step 'bar-step bar-step
'x-offset x-offset 'x-log-adj x-log-adj
]
return obj-parms
]
; --------------------
; plot functions (feed into draw dialect)
; --------------------
y-lines: func [
[catch]
y-data [block!] "Block of y values to plot"
/local
y-pt x-val val-pair scale-y y-val parms
][
parms: initialize ; get everything setup, if not already
scale-y: y-curry parms/y-scaled-zero parms/y-scaler ; set up the scaler function
x-val: parms/x-offset ; start the far left
insert tail result 'line
x-increment: (to-decimal parms/x-step) / (parms/x-log-adj)
foreach y-pt y-data [
; don't plot any none values (after incrementing x)
if y-pt [
val-pair: to-pair reduce [(to-integer x-val) (scale-y y-pt)]
insert tail result val-pair
]
x-val: (x-increment: x-increment * parms/x-log-adj) + x-val
]
insert tail result [line-pattern]
]
y-grids: func [
[catch]
p-size [pair!]
nr-lines [integer!]
/local
y-val parms x-offset scale-y y-inc
][
parms: initialize ; get everything setup
insert tail result [line-pattern none] ; ensure we draw continuous lines
scale-y: y-curry parms/y-scaled-zero parms/y-scaler ; set up the scaler function
y-inc: (to-decimal parms/y-max - parms/y-min ) / (nr-lines - 1)
x-offset: parms/x-offset
y-val: parms/y-min
y-pos: p-size/y - 1
; draw the line at the bottom of the plot
insert insert tail result 'line (to-pair reduce [
(x-offset) (to-integer y-pos)])
insert tail result (to-pair reduce [(p-size/x + x-offset) (to-integer y-pos)])
until [
y-val: y-val + y-inc
y-pos: scale-y y-val
insert insert tail result 'line (to-pair reduce [
(x-offset) (to-integer y-pos)])
insert tail result (to-pair reduce [(p-size/x + x-offset) (to-integer y-pos)])
y-pos < 0.0
]
]
x-grids: func [
[catch]
p-size [pair!]
nr-lines [integer!]
/local
x-val x-inc parms
][
parms: initialize ; get everything setup
x-val: parms/x-offset + 1
x-inc: (to-decimal p-size/x - 2) / (nr-lines - 1) / parms/x-log-adj
insert tail result [line-pattern] ; ensure continuous lines
until [
insert insert tail result first [line] (to-pair reduce [(to-integer x-val) 0])
insert tail result (to-pair reduce [(to-integer x-val) (p-size/y)])
x-val: (x-inc: parms/x-log-adj * x-inc) + x-val
x-val > (p-size/x + parms/x-offset + 1)
]
]
x-axis: func [
[catch]
p-size [pair!]
full-size [pair!]
nr-marks [integer!]
/local
x-step x-inc y-height x-val x-pos p-val x-adj parms i
][
parms: initialize ; get everything setup
; handle the special case when only x-data is only 2 items
; shorthand for setting x-min and x-max
if all [ x-data ((length? x-data ) = 2)] [
set-x-min: first x-data
set-x-max: second x-data
obj-parms: none
x-data: none
x-axis p-size full-size nr-marks
]
x-step: (to-decimal p-size/x) / (nr-marks - 1) / parms/x-log-adj
x-inc: either x-data [
(length? x-data) / nr-marks
][
either date? parms/x-min [
(parms/x-max - parms/x-min) / (nr-marks - 1)
][
(to-decimal parms/x-max - parms/x-min ) / (nr-marks - 1)
]
]
y-height: full-size/y - 25
x-val: parms/x-min
x-pos: parms/x-offset
insert insert tail result first [text] (to-pair reduce [(to-integer x-pos)
(y-height)])
insert tail result to-string parms/x-min
x-pos: x-pos
i: 0
until [
i: i + 1
x-val: either x-data [
pick x-data round-0 (i * x-inc)
][
parms/x-min + to-integer (x-inc * i)
]
x-pos: x-pos + x-step: (x-step * parms/x-log-adj)
insert insert tail result first [text] (to-pair reduce [
(to-integer x-pos) (y-height)])
insert tail result to-string either date? x-val [x-val][round-2 x-val]
x-pos > (full-size/x - (x-step))
]
if x-log-scale [return]
; add a final label for the max value (since it falls off the chart otherwise
; (this one is inset from the right end of the chart)
; (different inset for dates)
insert insert tail result first [text] (to-pair reduce [
(full-size/x - either date? parms/x-min [
55
][
9 * (1 + to-integer log-10 p-val: round-2 parms/x-max)
])
(y-height)])
insert tail result to-string either date? x-val [parms/x-max][p-val]
]
y-axis: func [
[catch]
p-size [pair!]
nr-marks [integer!]
/local
y-step y-inc x-offset y-val y-pos y-adj parms
][
parms: initialize ; get everything setup
scale-y: y-curry parms/y-scaled-zero parms/y-scaler ; set up the scaler function
y-inc: (to-decimal parms/y-max - parms/y-min ) / (nr-marks - 1)
x-offset: 1
y-val: parms/y-min
y-pos: p-size/y - 16
insert insert tail result first [text] (to-pair reduce [
(x-offset) (to-integer y-pos)])
insert tail result to-string round-2 parms/y-min
until [
y-val: y-val + y-inc
y-pos: scale-y y-val
insert insert tail result first [text] (to-pair reduce [
(x-offset) (to-integer y-pos)])
insert tail result to-string round-2 y-val
y-pos < 0.0
]
]
stock-ohlc: func [
[catch]
data [block!] "[open high low close] each is a block of values"
/local
opens highs lows closes x-tick x-pos scale-y parms x-inc
][
parms: initialize ; get everything setup
opens: first data
highs: second data
lows: third data
closes: fourth data
x-tick: to-integer( parms/x-step / 4 )
if (x-tick < 1) [x-tick: 1]
x-pos: parms/x-offset + x-tick
x-inc: parms/x-step
scale-y: y-curry parms/y-scaled-zero parms/y-scaler
until [
; skip plotting the value if it is none (drop down to getting the next elements)
if first closes [
; draw the day's high to low segment
insert insert tail result first [line] to-pair reduce [(to-integer x-pos)
(scale-y first highs)]
insert tail result to-pair reduce [(to-integer x-pos) (scale-y first lows)]
; draw the open segment to the right of the High-Low line
insert insert tail result first [line] to-pair reduce [
(to-integer (x-pos - x-tick)) (scale-y first opens)]
insert tail result to-pair reduce [(to-integer x-pos) (scale-y first opens)]
; draw the close segment to the left of the High-Low line
insert insert tail result first [line] to-pair reduce [(to-integer x-pos)
(scale-y first closes)]
insert tail result to-pair reduce [(to-integer(x-pos + x-tick))
(scale-y first closes)]
]
; step to the next element in each vector of prices
opens: next opens
highs: next highs
lows: next lows
closes: next closes
x-pos: x-pos + x-inc: (parms/x-log-adj * x-inc)
; test to see if we've reached the end of the data in
; a representative vector {opens}
tail? opens
]
insert tail result [line-pattern]
]
add_text: func [txt over up usr-font /local posit] [
parms: initialize
if usr-font [insert tail result compose [font (usr-font)] ]
posit: to-pair reduce [to-integer (p-size/x + parms/x-offset * over / 100)
to-integer (p-size/y * ( 100 - up / 100) )]
insert tail result compose [text (posit) (txt)]
insert tail result compose [font (default-font)]
]
add_label: func [/local x-pos y-pos scale-y parms label-step][
parms: initialize
label-nr: label-nr + 1
label-step: to-integer ((length? y-data) / 25 * label-nr)
if ( label-step < 2 ) [label-step: 2]
x-pos: to-integer label-step * parms/x-step + parms/x-offset
if (x-pos > (p-size/x - 25 - parms/x-offset )) [
label-nr: 1
add_label
]
scale-y: y-curry parms/y-scaled-zero parms/y-scaler
y-pos: scale-y pick y-data label-step
insert insert tail insert tail result 'text to-pair reduce [x-pos y-pos] label-txt
]
reset-init: does [
obj-parms: none
]
full-reset-init: does [
obj-parms: none
set-x-min: none
set-x-max: none
set-y-min: none
set-y-max: none
]
bar-graph: func [
[catch]
"draw a set of bar graphs"
data [block!]
p-size [pair!]
/local
x-pos low-left up-right val y-scale bar-step parms
][
parms: initialize ; get everything setup, if not already
if (not bar-width) [
bar-width: (p-size/x / ((length? y-data) + 1 ))
]
bar-step: parms/bar-step
x-pos: parms/x-offset ; use the line below to have the edge bars "fall off"
; x-pos: parms/x-offset - bar-width / 2.0 ; also need to make the change to 'bar-width
y-scale: y-curry parms/y-scaled-zero parms/y-scaler
foreach val data [
; skip over the data if it is none (drop down to increment x-pos)
if val [
low-left: to-pair reduce [(to-integer x-pos) (min y-scale parms/y-min y-scale 0)]
up-right: to-pair reduce [( to-integer (x-pos + bar-width)) (y-scale val)]
insert insert insert tail result first [box] low-left up-right
]
x-pos: x-pos + parms/bar-step
]
insert tail result [line-pattern fill-pen]
]
stock-candles: func [
[catch]
data [block!] "[open high low close] each is a block of values"
up-color
down-color
/local
opens highs lows closes x-tick x-pos scale-y parms open-left close-right day-color
][
parms: initialize ; get everything setup
opens: first data
highs: second data
lows: third data
closes: fourth data
x-tick: to-integer( parms/x-step / 4 )
if (x-tick < 1) [x-tick: 1]
x-pos: parms/x-offset + x-tick
x-inc: parms/x-step
scale-y: y-curry parms/y-scaled-zero parms/y-scaler
until [
; skip plotting the value if it is none (drop down to getting the next elements)
if first closes [
; determine the color for the day (if an up or down day)
day-color: either ((first opens) <= (first closes)) [up-color][down-color]
append result compose [fill-pen (day-color) pen (day-color)]
; draw the day's high to low segment
insert insert tail result first [line] to-pair reduce [(to-integer x-pos)
(scale-y first highs)]
insert tail result to-pair reduce [(to-integer x-pos) (scale-y first lows)]
; get the corners of the box representing the candle body
open-left: to-pair reduce [
(to-integer (x-pos - x-tick)) (scale-y first opens)]
close-right: to-pair reduce [(to-integer (x-pos + x-tick))
(scale-y first closes)]
; draw the candle body over the high-low line
insert insert insert tail result 'box open-left close-right
]
; step to the next element in each vector of prices
opens: next opens
highs: next highs
lows: next lows
closes: next closes
x-pos: x-pos + x-inc: (parms/x-log-adj * x-inc)
; test to see if we've reached the end of the data in
; a representative vector {opens}
tail? opens
]
insert tail result [line-pattern pen default-color fill-pen]
]
scatter: func [
data-blk [block!] {data block x-y tuples/blocks/pairs}
shape [word!] {shape for the symbol to plot}
sym-size [integer!] {size of the symbol to plot}
/local
x-diff x-scaled-zero x-scaler x-pt y-pt pt
][
x-data: copy []
y-data: copy []
foreach pt data-blk [
insert tail x-data pt/1
insert tail y-data pt/2
]
parms: initialize
; ---------------------------------------------
; set up x scaling parameters
; ---------------------------------------------
x-diff: to-decimal either x-log-scale [
either any [ parms/x-max = 0 parms/x-min = 0] [
log-10 (parms/x-max - parms/x-min)
][
either any [(parms/x-min >= 0.0) (parms/x-max <= 0.0)] [
; min and max are both have the same sign
abs ((log-10 abs parms/x-max) - log-10 (abs parms/x-min))
][
; min and max cross zero
(log-10 parms/x-max) + log-10 abs parms/x-min
]
]
][
parms/x-max - parms/x-min
]
x-scaler: (to-decimal p-size/x) / x-diff
; start computing the x-scaled-zero
either x-log-scale [
either parms/x-min = 0.0 [x-scaled-zero: 0.0][
x-scaled-zero: x-scaler * log-10 (abs parms/x-min)
]
if parms/x-min < 0 [
x-scaled-zero: negate x-scaled-zero
if parms/x-max > 0 [
x-scaled-zero: (negate parms/x-max / (parms/x-max + parms/x-max) * p-size/x)
]
]
][
x-scaled-zero: (negate x-scaler * parms/x-min)
]
scale-x: x-curry x-scaled-zero x-scaler
scale-y: y-curry parms/y-scaled-zero parms/y-scaler
until [
y-pt: first y-data
x-pt: first x-data
y-data: next y-data
x-data: next x-data
pt: to-pair reduce [(parms/x-offset + scale-x x-pt ) (scale-y y-pt)]
append result switch shape [
circle [plot-circle pt sym-size]
box [plot-box pt sym-size]
diamond [plot-diamond pt sym-size]
cross [plot-cross pt sym-size]
X-mark [plot-X-mark pt sym-size]
point [plot-circle pt 1]
]
tail? y-data
]
x-data: none
y-data: head y-data
]
; symbol plotting functions
plot-circle: func [pt [pair!] size [integer!] /local
][
return reduce ['circle pt size]
]
plot-box: func [pt [pair!] size [integer!] /local
up-left low-rt
][
up-left: pt - size
low-rt: pt + size
return reduce ['box up-left low-rt]
]
plot-X-mark: func [pt [pair!] size [integer!] /local
up-left low-left up-rt low-rt
][
up-left: pt - size
low-rt: pt + size
up-rt: to-pair reduce [low-rt/x up-left/y]
low-left: to-pair reduce [up-left/x low-rt/y]
return reduce ['line up-left low-rt 'line low-left up-rt]
]
plot-cross: func [pt [pair!] size [integer!] /local
up-pt low-pt rt-pt left-pt
][
up-pt: to-pair reduce [pt/x (pt/y - size)]
low-pt: to-pair reduce [pt/x (pt/y + size)]
left-pt: to-pair reduce [(pt/x - size) pt/y]
rt-pt: to-pair reduce [(pt/x + size) pt/y]
return reduce ['line up-pt low-pt 'line left-pt rt-pt]
]
plot-diamond: func [pt [pair!] size [integer!] /local
up-pt low-pt rt-pt left-pt
][
up-pt: to-pair reduce [pt/x (pt/y - size)]
low-pt: to-pair reduce [pt/x (pt/y + size)]
left-pt: to-pair reduce [(pt/x - size) pt/y]
rt-pt: to-pair reduce [(pt/x + size) pt/y]
return reduce ['polygon up-pt rt-pt low-pt left-pt up-pt]
]
pie: func [ data [block!]
p-size [pair!]
r-pct [integer!] "pct of full size"
labels-blk [block!] "data labels"
exp-blk [block!] "block with the number of each slice to explode"
/local
r midpoint sum pcts pct val x y val2 r2
][
fills: [
gold
teal
olive
brick
pink
water
purple
violet
khaki
brown
oldrab
leaf
coffee
tan
magenta
navy
orange
aqua
forest
maroon
]
midpoint: p-size / 2
r: min midpoint/x midpoint/y
sum: 0
foreach val data [
sum: sum + val
]
pcts: copy []
foreach val data [
append pcts (to-decimal val / sum)
]
append result compose [fill-pen (back-color)]
append result compose [circle (midpoint) (r: r * r-pct / 100)]
pt: to-pair reduce [midpoint/x to-integer midpoint/y - r]
append result compose [line (midpoint) (pt)]
count: 0
val: 0.0 ; start at 0 degrees rotation.
; Y plots down from the top so its minus
foreach pct pcts [
count: count + 1
if pct > 0 [
fil-col: first fills
old-val: val
val2: (pct * 180.0 ) + val
val: (pct * 360.0) + val
x: r * sine val
y: r * cosine val
pt: to-pair reduce [(to-integer midpoint/x + x) (to-integer midpoint/y - y)]
x: (r - 5) * sine val2
y: (r - 5) * cosine val2
fill-pt: to-pair reduce [(to-integer midpoint/x + x) (to-integer midpoint/y - y)]
x: (r + 10) * sine val2
y: (r + 10) * cosine val2
label-pt: to-pair reduce [(to-integer midpoint/x + x) (to-integer midpoint/y - y)]
append result compose [line (midpoint) (pt)]
;; process the section if it is on the exploded list
if find exp-blk count [
r*: 100 - r-pct / 100 * r
x*: r* * sine val2
y*: r* * cosine val2
mid*: to-pair reduce [to-integer midpoint/x + x* to-integer midpoint/y - y*]
; note that x and y are still based on r + 10 and val2
label-pt: to-pair reduce [to-integer mid*/x + x to-integer mid*/y - y]
append result compose [fill-pen (fil-col)]
append result compose [polygon (mid*)]
for theta old-val val 5 [
x: r * sine theta
y: r * cosine theta
pt: to-pair reduce [to-integer mid*/x + x to-integer mid*/y - y]
append result pt
]
append result mid*
]
append result compose [fill-pen (fil-col)]
append result compose [flood (fill-pt)]
if not error? try [txt: first labels-blk][
append result compose [text (label-pt) (to-string txt)]]
if tail? fills: next fills [fills: head fills]
labels-blk: next labels-blk
]
]
]
; this returns a function that gives the properly scaled y value
; the function is different if we are using a log scale
y-curry: func [
"y-scaler curried with y-scaling constants"
y-scaled-zero [decimal!]
y-scaler [decimal!]
][
either y-log-scale [
return func [y][
if (y = 0) [return to-integer y-scaled-zero]
return either ( y > 0) [
to-integer (y-scaled-zero - (y-scaler * log-10 y))
][
to-integer (y-scaled-zero + (y-scaler * log-10 negate y))
]
]
][
return func [y /local val][
to-integer (y-scaled-zero - (y * y-scaler) )
]
]
]
; do the same for x values (only when needed -- scatter)
x-curry: func [
"x-scaler curried with x-scaling constants"
x-scaled-zero [decimal!]
x-scaler [decimal!]
][
either x-log-scale [
return func [x][
if (x = 0) [return to-integer x-scaled-zero]
return either ( x > 0) [
to-integer (x-scaled-zero + (x-scaler * log-10 x))
][
to-integer (x-scaled-zero - (x-scaler * log-10 negate x))
]
]
][
return func [x][
to-integer (x-scaled-zero + (x * x-scaler) )
]
]
]
round-2: func [
val [number!]
][ ; round by shifting decimal and truncating
either val < 0 [
if val < -100000 [return to-integer val] ; needed so we don't overflow
return (to-integer ((val * 100) - 0.5))/ 100.0
][
if val > 1000000 [return to-integer val]
return (to-integer ((val * 100) + 0.5)) / 100.0
]
]
round-0: func [
val [number!]
][
; round by shifting decimal and truncating
either val < 0 [
return to-integer val - 0.5
][
return to-integer val + 0.5
]
]
set 'quick-plot func [
{Implement a quick and easy plotting dialect to feed into View/Draw}
[catch]
cmds [block!] "Input block for processing"
/local
shape sym-size data-blk
][
; --------------------
; Re-initialize key words to none
; and reset all the defaults
; --------------------
obj-parms: none
set-x-min: none
set-x-max: none
set-y-min: none
set-y-max: none
title-vals: copy []
usr-font: none
bar-width: none
x-log-scale: false
y-log-scale: false
x-border: false
y-border: false
r-pct: 80
lab-blk: copy []
exp-blk: copy []
y-data: none
x-data: none
result: none
x-pct: 0
y-pct: 0
shape: 'x-mark
sym-size: 3
label-nr: 0
default-font: make face/font [
size: 14
; style: [bold]
name: font-sans-serif
]
default-color: black
default-fill: gray
back-color: rebolor
up-color: silver
down-color: 45.45.45
border-width: 35
x-border: false
y-border: false
x-log-scale: false
y-log-scale: false
dynamic-scale: false
dyn-pct: 100
title-style: 'h1
; -------------------------
; Define the parse rules
; There is one rule for every option in the dialect
; -------------------------
; plot rules
scale-cmd: ['scale ['log (y-log-scale: true x-log-scale: false) |
'log-linear (x-log-scale: false y-log-scale: true) |
'log-log (x-log-scale: y-log-scale: true) |
'linear (x-log-scale: y-log-scale: false) |
'dynamic opt [set dyn-pct integer!] (dynamic-scale: true)
] (reset-init)]
plot-x-axis-cmd: ['x-axis x-axis-opts ]
plot-y-axis-cmd: ['y-axis y-axis-opts ]
x-axis-opts: [any [ ['inset (y-border: false) | 'border (y-border: true) ] |
set nr-marks integer! ] ]
y-axis-opts: [any [ ['inset (x-border: false) | 'border (x-border: true) ] |
set nr-marks integer! ] ]
plot-rules: [ any [
scale-cmd |
plot-x-axis-cmd |
plot-y-axis-cmd |
skip
]
]
; element rules
line-cmd: ['line set y-data block! opt [color-cmd] (y-lines y-data)]
bar-cmd: ['bars any [color-cmd | fill-cmd | set y-data block!] (bar-graph y-data p-size)]
stock-cmd: ['stock set stock-data block! opt [color-cmd] (stock-ohlc stock-data)]
candles-cmd: ['candles any [ 'up set up-color [tuple! | word!] |
'down set down-color [word! | tuple!] | color-cmd |
set stock-data block! ]
(stock-candles stock-data up-color down-color)]
scatter-cmd: ['scatter any ['symbol set shape [word!] | 'size set sym-size [integer!] |
color-cmd | fill-cmd | set data-blk block!] (scatter data-blk shape sym-size) ]
pie-cmd: ['pie set pie-data block! any ['labels set lab-blk block! |
'explode set exp-blk block! | 'size set r-pct integer!]
(pie pie-data p-size r-pct lab-blk exp-blk)]
x-data-cmd: ['x-data set x-data block!]
x-grd-cmd: ['x-grid set nr-lines integer! (x-grids p-size nr-lines)]
y-grd-cmd: ['y-grid set nr-lines integer! (y-grids p-size nr-lines)]
x-axis-cmd: ['x-axis x-axis-opts (x-axis p-size full-size nr-marks)]
y-axis-cmd: ['y-axis y-axis-opts (y-axis p-size nr-marks)]
title-cmd: ['title any ['style set title-style word! | set title string!] ]
y-min-cmd: ['y-min set set-y-min number! (reset-init)]
y-max-cmd: ['y-max set set-y-max number! (reset-init)]
x-min-cmd: ['x-min set set-x-min number! (reset-init)]
x-max-cmd: ['x-max set set-x-max number! (reset-init)]
b-width-cmd: ['bar-width set bar-width integer!]
label-cmd: ['label set label-txt string! (add_label)]
text-cmd: ['text some [posit-cmd | font-cmd | color-cmd |
set txt string! ] (add_text txt x-pct y-pct usr-font) ]
misc-cmd: [set misc [word! | tuple! | number!] (append result misc)]
draw-cmd: ['draw set misc block! (append result misc)]
rescale-cmd: ['rescale (full-reset-init)]
pattern-cmd: ['pattern (append result pattern) any [set misc integer! (append result misc)] ]
color-cmd: ['color set misc [tuple! | word!] (append append result 'pen misc)]
fill-cmd: ['fill set misc [tuple! | word!] (append append result 'fill-pen misc)]
posit-cmd: ['over set x-pct integer! | 'up set y-pct integer!]
font-cmd: ['font set usr-font word!]
font-set-cmd: ['font set new-font word! (append append result 'font new-font)]
rules: [ any [
line-cmd |
bar-cmd |
stock-cmd |
candles-cmd |
scatter-cmd |
pie-cmd |
x-data-cmd |
x-grd-cmd |
y-grd-cmd |
x-axis-cmd |
y-axis-cmd |
y-min-cmd |
y-max-cmd |
x-min-cmd |
x-max-cmd |
b-width-cmd |
title-cmd |
label-cmd |
text-cmd |
rescale-cmd |
scale-cmd | ; need this to properly go through all the cmds
draw-cmd |
misc-cmd ; this needs to be last with no bar following it
]
end
]
; -------------------------
; Prepare values for parsing and processing
; -------------------------
reset-init ; get all the values zeroed out
; compose the input block to process any embedded REBOL
; throwing any errors in the process
if error? set/any 'err try [cmds: compose/deep bind cmds 'quick-plot] [
throw err
]
full-size: first cmds ; extract the size of the plot
if (not pair? full-size) [
throw make error! "first value of the input block must be the plot size (pair!)"
]
cmds: next cmds
result: copy []
; ---------------------------------------------
; insert our defaults into the result block
; ---------------------------------------------
append result copy compose [pen (default-color) fill-pen (default-fill)]
append result copy compose [font (default-font)]
; -------------------------
; parse and process the dialect
; -------------------------
; first the plot rules (effect the entire plot)
test: parse cmds plot-rules
; now set the pot size and scales (since they effect all elements)
p-size: full-size
if x-border [p-size/x: (p-size/x - border-width)]
if y-border [p-size/y: (p-size/y - border-width)]
; now the element rules
test: parse cmds rules
; if there was an error in parsing the cmds then throw an
; error to the user
if not test [throw make error! "Unable to parse commands"]
; instead of returning a block to feed draw run the process
; all the way through layout (simpler for the user)
; if we have title text add it to the plot
if title [
title-vals: copy []
; approximately center the title
; each character using h1 font is about 7.5 pixels wide. So we shift
; to the middle less 4 pixels times the number of characters
; to start drawing the title.
title-pos: to-integer ((p-size/x / 2 ) - (4 * length? title))
title-pos: to-pair reduce [title-pos 0]
insert insert tail title-vals compose [origin (title-pos) text (title-style)] title
title: none
]
out-obj: do reduce compose/deep [
; create the layout object we will return to the user
layout [
size (full-size)
origin 0x0
box (back-color) (full-size) effect [
draw [(result)]
]
(title-vals)
]
]
return out-obj
]
set 'multi-plot func [
[catch]
p-size [pair!] "size of the complete multi-plot"
plots [block!] "Block of quick-plot data blocks"
/across "layout the plots side by side"
/down "layout the plots down the page"
/ratio
ratios [block!] "block of relative sizes for the subplots"
/local
nr-plots this-sizer panes-blk box-size subplot this-img total-ratio
this-origin val subplot-size next-origin-offset
][
nr-plots: length? plots
if ratio [if not-equal? length? ratios nr-plots [
throw make error! "Ratio block must have an entry for each plot"
]
total-ratio: 0
foreach val ratios [
total-ratio: total-ratio + val
]
]
panes-blk: copy compose [size (p-size)]
either across [
append panes-blk 'across
box-size: to-pair reduce [2 (p-size/y + 10)]
][
append panes-blk 'below
box-size: to-pair reduce [(p-size/x + 10) 2]
]
this-origin: 0x0
forall plots [
subplot: first plots
either ratio [
this-sizer: total-ratio / first ratios
ratios: next ratios
][
this-sizer: nr-plots
]
either across [
subplot-size: to-pair reduce [to-integer (p-size/x / this-sizer - 4) p-size/y]
next-origin-offset: to-pair reduce [to-integer (p-size/x / this-sizer ) 0]
][
subplot-size: to-pair reduce [p-size/x to-integer (p-size/y / this-sizer - 4)]
next-origin-offset: to-pair reduce [0 to-integer (p-size/y / this-sizer )]
]
if ((type? first subplot) = pair!) [remove subplot]
this-img: to-image quick-plot (head insert subplot subplot-size)
append panes-blk compose [origin (this-origin) image (copy this-img)]
this-origin: this-origin + next-origin-offset
if (not tail? next plots) [
append panes-blk compose [origin (this-origin - 2) box (box-size) coal]
]
]
layout panes-blk
] ;multi-plot
]