#------------------------------------------------------------------------------- # UnleashCharts Free Ruby Charting library # Histograms , X-Y plots, Time Series charts # # Copyright (C) 2005, Unleash Networks P Ltd, All rights reserved # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #-------------------------------------------------------------------------------- module UnleashCharts USECPERSEC=1000000 MSECPERSEC=1000 # # UnChartBase - Base Class for all Charts # class UnChartBase < FXCanvas def initialize(parent) # parent super(parent,nil,0,LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT) # defaults @backColor = "black" @drawColor = "white" @font = FXFont.new(getApp(), "verdana", 8, FONTWEIGHT_NORMAL) @fonttitle = FXFont.new(getApp(), "verdana", 10, FONTWEIGHT_BOLD) @fontsmall = FXFont.new(getApp(), "verdana", 6, FONTWEIGHT_NORMAL) @title=nil # connect events to this canvas self.connect(SEL_PAINT) do |sender,sel,event| procPaint(sender,sel,event) end end def create super @font.create @fontsmall.create @fonttitle.create end def setModel (mod) @model = mod update repaint end def drawGrid(hdc, crect, nlines) oldstyle=hdc.lineStyle oldcolor=hdc.foreground hdc.lineStyle = LINE_ONOFF_DASH hdc.foreground = FXRGB( 128,128,128) yinc = crect.h/nlines ypos = crect.y (1..nlines).each do |lnum| hdc.drawLine( crect.x, ypos, crect.x+crect.w,ypos) ypos+=yinc end hdc.lineStyle=oldstyle hdc.foreground=oldcolor end def drawTitle(dc, crect) if !@title return end dc.font = @fonttitle tw=@fonttitle.getTextWidth(@title) dc.drawText((crect.w - tw)/2, 20, @title) end private def setscales( gridrect , max_x, min_y, max_y ) ra = RangeAdjuster.new #print "input model min_y = #{min_y}\n" max_y=ra.adjustMax(min_y,max_y+ max_y*5/100) min_y=ra.adjustMin(min_y,max_y+ max_y*5/100) #print "adj max_y = #{max_y}\n" #print "adj min_y = #{min_y}\n" @scale_x = (gridrect.w.to_f/max_x.to_f) @scale_y = (gridrect.h.to_f/(max_y.to_f-min_y.to_f) ) @yscalelabels=ra.getLabels(min_y,max_y,5) # @yscalelabels.each { |s| print "#{s}\n" } @maxadj_y = max_y @minadj_y = min_y end def scalex(val) val * @scale_x end def scaley(val) (val - @minadj_y) * @scale_y end # public member attributes public attr_accessor :backColor , :drawColor, :barColor, :font attr_accessor :model attr_accessor :title # instance variables local protected @scale_x @scale_y @maxadj_y @minadj_y @yscalelabels @fontsmall end # # A Bar Chart # class UnBarChart < UnChartBase def initialize(parent) super(parent) @barColor="yellow" end def procPaint(sender,sel,event) dc = FXDCWindow.new(self) # -------------------- # clear the background # -------------------- dc.foreground = @backColor dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h) # axis dc.foreground = @drawColor crect = event.rect crect.shrink!(40,30) dc.drawLine(crect.x,crect.y+crect.h,crect.x+crect.w,crect.y+crect.h) # x axis dc.drawLine(crect.x,crect.y,crect.x,crect.y+crect.h) # y axis if !@model dc.end return end # set scales setscales(crect, @model.barcount, 0, @model.maxval) # draw scales # x-scale dc.font = @fontsmall @model.each_label_x do |labstr,labval| dc.drawText( crect.x + scalex(labval)-5 , crect.y + crect.h + 15, labstr) end # y-scale yval=0 ystep=@maxadj_y.to_f/5 @yscalelabels.each do |lbl| dc.drawText( crect.x - 30, crect.y+crect.h-scaley(yval), lbl) yval += ystep end # draw grid drawGrid(dc,crect,5) # draw title drawTitle(dc,crect) # draw values dc.font = @font @model.each_val do |xval, yval| xpos = crect.x + scalex(xval) ypos = scaley(yval) dc.foreground = @barColor dc.fillRectangle(xpos,crect.y+crect.h-ypos,30,ypos) dc.foreground = @drawColor dc.drawText(xpos+5,crect.y+crect.h-ypos-5,yval.to_s) end dc.end end public attr_writer :barColor end # # Data Point Style # class DataPointStyle public LINEHEIGHT = 0 POINTCHAR = 1 LINECONNECT = 2 LINECHAR = 3 attr_accessor :type attr_accessor :point_char attr_accessor :color def initialize @type = LINEHEIGHT @pointchar = "o" @color = nil end def initialize( ty, pc) @type = ty @point_char = pc @color = nil end def initialize( ty, pc, col = nil) @type = ty @point_char = pc @color = col end end # # A Time Series Chart # class UnTimeSeriesChart < UnChartBase def initialize(parent) # parent super(parent) end def procPaint(sender,sel,event) dc = FXDCWindow.new(self) # -------------------- # clear the background # -------------------- dc.foreground = @backColor dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h) # axis dc.foreground = @drawColor crect = FXRectangle.new(0,0,sender.width,sender.height) crect.shrink!(40,30) dc.drawLine(crect.x,crect.y+crect.h,crect.x+crect.w,crect.y+crect.h) # x axis dc.drawLine(crect.x,crect.y,crect.x,crect.y+crect.h) # y axis if !@model dc.end return end # set scales x (total time ) and y (max y value) setscales(crect, @model.totalTimeUSecs,@model.minval, @model.maxval) # draw scales # x-scale drawTimeScale( dc, crect) # y-scale labels from min to max yval=@minadj_y ystep=(@maxadj_y.to_f-@minadj_y.to_f)/5 @yscalelabels.each do |lbl| use_lab = lbl + @model.valunits use_lab.gsub!("Ku","m") use_lab.gsub!("Mu"," ") dc.drawText( crect.x - 30, crect.y+crect.h-scaley(yval), use_lab) yval += ystep end # draw grid drawGrid(dc,crect,5) # draw title drawTitle(dc,crect) # draw values dc.font = @font startTime = @model.startTime startTimeUSecs = startTime.tv_sec*USECPERSEC+startTime.tv_usec dc.foreground = @drawColor oldxpos,oldypos = 0,0 @model.each_val do |timeval, yval, style| xpos = crect.x + scalex((timeval.tv_sec*USECPERSEC+timeval.tv_usec) - startTimeUSecs) ypos = scaley(yval) if style if style.color oldclr = dc.foreground dc.foreground = style.color end if style.type == DataPointStyle::LINEHEIGHT dc.drawLine(xpos,crect.y+crect.h,xpos,crect.y+crect.h-ypos) elsif style.type == DataPointStyle::POINTCHAR dc.drawText(xpos,crect.y+crect.h-ypos, style.point_char) elsif style.type == DataPointStyle::LINECONNECT if oldxpos + oldypos > 0 dc.drawLine(oldxpos,oldypos,xpos,crect.y+crect.h-ypos) end elsif style.type == DataPointStyle::LINECHAR if oldxpos + oldypos > 0 dc.drawLine(oldxpos,oldypos,xpos,crect.y+crect.h-ypos) dc.drawText(xpos,crect.y+crect.h-ypos+5, style.point_char) end end if style.color dc.foreground = oldclr end else # the default style is a line dc.drawLine(xpos,crect.y+crect.h,xpos,crect.y+crect.h-ypos) end oldxpos,oldypos =xpos,crect.y+crect.h-ypos end # label the axes dc.drawText(5,15,@model.yscalelabel) dc.drawText(crect.x+crect.w-10, crect.y+crect.h+20,@model.xscalelabel) dc.end end def setModel (mod) @model = mod update repaint() end def drawTimeScale(dc,crect) dc.font = @fontsmall firstLabelLine1 = @model.startTime.strftime("%x") firstLabelLine2 = @model.startTime.strftime("%I:%M:%S%p-") firstLabelLine2 += @model.startTime.tv_usec.to_s dc.drawText( crect.x - 30, crect.y+crect.h+ 12, firstLabelLine1) dc.drawText( crect.x - 30, crect.y+crect.h+ 20, firstLabelLine2) # right now very simple , just try from a preset list of scales for a match candidate_scales = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000] multiplier = 1 scale_lab = "us" nTicks = 5 intervalUS = @model.totalTimeUSecs if intervalUS / USECPERSEC > nTicks scale_lab = "s" multiplier = USECPERSEC elsif intervalUS / MSECPERSEC > nTicks scale_lab = "ms" multiplier = MSECPERSEC end use_scale=1 candidate_scales.each do |s| nt = intervalUS / (s * multiplier) if use_scale==1 && nt <= nTicks use_scale = s end end currUS = 0 i =0 while currUS <= intervalUS xpos = crect.x + scalex(currUS) ypos = crect.y + crect.h + 15 tick = use_scale * i lab = "+ #{tick} #{scale_lab}" if i > 0 dc.drawText(xpos,ypos,lab) end currUS = currUS + use_scale * multiplier i = i + 1 end end end class RangeAdjuster def adjustScales(min, max) dmin=adjustMin(min,max) dmax=adjustMax(min,max) min,max=dmin,dmax end def getLabels(min,max,labelstep) vLabels = Array.new famax = adjustMax(min,max).to_f famin = adjustMin(min,max).to_f fastep = (famax-famin)/labelstep (0..labelstep).each do |i| vLabels<<getFormatted( famin+fastep*i) end getLabels=vLabels end def getFormatted(dval) unit=" "; dchop=dval fmt="" if (dval>1000000000) unit,dchop ="G",(dval/1000000000).to_f elsif (dval > 1000000) unit,dchop ="M",(dval/1000000).to_f elsif (dval > 1000) unit,dchop ="K",(dval/1000).to_f end if (dchop.to_i*100) == (dchop.to_f*100) fmt=sprintf("%u%s",dchop,unit) elsif (dchop.to_i*10) == (dchop.to_f*10) fmt=sprintf("%.1f%s",dchop,unit) else fmt=sprintf("%.1f%s",dchop,unit) end getFormatted=fmt end def adjustMax(dmin,dmax) mul=1.0 if dmax==dmin dmax=dmin+1 end #print "adjustMax dmax = #{dmax} dmin = #{dmin}\n" if dmax > 1000000000 dmax = (dmax.to_f / 1000000000) dmin = dmin.to_f / 1000000000 mul = 1000000000 elsif dmax > 1000000 dmax = (dmax.to_f / 1000000) dmin = dmin.to_f / 1000000 mul = 1000000 elsif dmax > 1000 dmax = (dmax.to_f / 1000) dmin = dmin.to_f / 1000 mul = 1000 end getCeil(dmax, 10** getOrder(dmax-dmin)) * mul end def adjustMin(dmin,dmax) mul=1.0 if dmax-dmin < 10 return dmin * 10 end if dmin > 1000000000 dmax = dmax.to_f / 1000000000 dmin = dmin.to_f / 1000000000 mul = 1000000000 elsif dmin > 1000000 dmax = dmax.to_f / 1000000 dmin = dmin.to_f / 1000000 mul = 1000000 elsif dmin > 1000 dmax = dmax.to_f / 1000 dmin = dmin.to_f / 1000 mul = 1000 end adjustMin = getFloor(dmin, 10**getOrder(dmax-dmin)) * mul end def getOrder(d) if d<=0 0 else Math.log10(d).floor end end def getFloor(d,l) v = d/l l * v.floor end def getCeil(d,l) v = (d.to_f/l.to_f).ceil #print "getceil d=#{d} v=#{v} l=#{l}\n" l * v end end # of class RangeAdjuster end # of module UnleashCharts