[= Demo - Mulitple Bar Command: multibar.bat Purpose: Example of a bar chart with multiple bars in each group. For this example we are assuming a known fixed number of bars ( 4 ). To avoid cluttering the code with irrelavent layout calculations some values have been fudged by hand. All coordinates are relative to the screen origin. This is acceptable for a simple example like this. In this example all error will be passed up to the main program. =] [= Global layout values =] Private Const Number MARGIN = 40; [= margin to left and right of plot =] Private Const Number NUMPARTIES = 3; [= number of actual quoted parties =] Private Const Number MINBAROFF = 10; [= minimum bar offset ( create gap between groups ) =] [= types for the data =] Type SeatsRecord = { Time date, Number seats, Number lab, Number con, Number lib }; Type SeatsArray = SeatsRecord[]; [= record to keep all the layout information =] Type TLayout = { [= heading region =] Number headheight, [= plot region =] Point plotorigin, Number plotwidth, Number plotheight, Number groupwidth, Number barwidth, Number baroffset, Number ybase, Number yscale, Number maxseats, [= key region =] Point keyorigin, Number keyheight }; [= set up the pens for the bars =] TPen LabPen = Pen {colour -> { 100, 0, 0 } }; [= red =] TPen ConPen = Pen {colour -> { 0, 0, 100 } }; [= blue =] TPen LibPen = Pen {colour -> { 100, 100, 0 } }; [= yellow =] TPen OthPen = Pen {colour -> { 50, 50, 50 } }; [= grey =] [= PROGRAM ------- The main program reads the data then plots it. =] Program() SeatsArray results; TLayout layout; Begin [= read in the fixed data file using a standard format file =] results := ReadData("EXAMPLES:/multibar.txt", "EXAMPLES:/fmt1hymd.ini"); [= do all the layout calcuations =] layout := CalcLayout(results, Canvas); [= draw the main chart heading =] Heading(); [= plot the main chart =] PlotData(results, layout ) => layout.plotorigin; [= draw the party key =] Key(layout) => layout.keyorigin; OnError [= we really should not get here =] Output "**** unexpected error"; Output Status; End; [= PLOT DATA --------- Plot the actual data. =] Shape PlotData(Const Ref SeatsArray results, Const Ref TLayout layout) Number group = 0; Begin [= draw the verticla axis =] VertAxis(layout); [= go through the data array =] Over results As entry Do [= plot one group . i.e. one set of election results =] PlotGroup(entry, layout) => {layout.groupwidth * group, 0} ; [= advance along the axis =] group += 1; EndOver; End; [= HEADING ------- Write the main heading. As the fonts are only used here there in no point making them global. =] Shape Heading() [= create some special font for the headings =] TFont headfont = Font { size -> 24 }; TFont subfont = Font { size -> 20, italic -> True }; Begin [= write the main heding =] TextBlock("Recent UK General Election Results", halign -> Centre, valign -> Top) => { Canvas.width // 2, 20 }, headfont; [= write the sub-headng =] TextBlock("Seats won by major parties", halign -> Centre, valign -> Top) => { Canvas.width // 2, 50 }, subfont; End; [= KEY --- This is the key linking the colour to the name. The location ( origin ) of the key is set by the caller. All coordinates will be local. =] Shape Key(TLayout layout) [= divide the space evenly =] Number width = ( layout.plotwidth - 2 * MARGIN ) // 4; Begin [= draw all the keys - do the layout calculation here =] KeyItem("Labour", LabPen); KeyItem("Conservative", ConPen) => { width, 0 }; KeyItem("Liberal/LibDem", LibPen) => { 2 * width, 0 }; KeyItem("Others", OthPen) => { 3 * width, 0 }; End; [= KEY ITEM -------- The blockpen is used for the square, the text is written with the inherited pen. The origin is the top left of the region. =] Shape KeyItem(Text name, TPen blockpen) Number boxsize = 20; Begin [= draw the party colour =] Rectangle(boxsize, boxsize, Solid) => blockpen; [= write the party name =] TextBlock(name, valign -> Centre) => {boxsize + 10, boxsize // 2}; End; [= PLOT GROUP ---------- Plot one group ( result for one election ). This routine could easyly be generalised to handle an arbitrary number of bars ( see the multiple line example ). =] Shape PlotGroup(SeatsRecord results, Const Ref TLayout layout) Number totalsofar = 0; Text date; Begin [= process the first party's results =] Rectangle(layout.barwidth-1, -results.lab * layout.yscale, Solid) => { layout.baroffset, 0 }, LabPen; totalsofar := results.lab; [= process the second party's results =] Rectangle(layout.barwidth-1, -results.con * layout.yscale, Solid) => { layout.baroffset + layout.barwidth, 0 }, ConPen; totalsofar += results.con; [= process the third party's results =] Rectangle(layout.barwidth-1, -results.lib * layout.yscale, Solid) => { layout.baroffset + 2 * layout.barwidth, 0 }, LibPen; totalsofar += results.lib; [= write the remainder as the 'others' =] Rectangle(layout.barwidth-1, -(results.seats - totalsofar) * layout.yscale, Solid) => { layout.baroffset + 3 * layout.barwidth, 0 }, OthPen; [= write the date as just month and year =] date := Format(results.date, "N Y4"); TextBlock(date, halign -> Centre, valign -> Top ) => { layout.groupwidth // 2, 10 }; End; [= VERT AXIS --------- Draw and label the vertical ( seats ) axis. Include the grid lines. The origin will be the grid origin. =] Shape VertAxis(TLayout layout) Const Colour GREY = { 50, 50, 50 }; TPen thinpen = Pen { colour -> GREY }; TPen thickpen = thinpen { width -> 3 }; Number seats, height; Begin [= do all at intevals of 25 seats =] For seats From 0 To layout.maxseats Step 25 Do height := -(seats * layout.yscale); [= draw the line - multiples of 100 use thick pen =] If seats Mod 100 == 0 And seats <> 0 Then [= write the lable =] TextBlock(Format(seats), halign -> Right, valign -> Centre ) => {-10, height}; [= draw a thick line =] Line( { 0, height }, { layout.plotwidth, height } ) => thickpen; Else [= just draw the thin line =] Line( { 0, height }, { layout.plotwidth, height } ) => thinpen; EndIf; EndFor; End; [= CALC LAYOUT ----------- Calculate the layout values that depend on number of records. Note not explicitly checking for zero records - will rely on error trapping. =] Function TLayout CalcLayout(Const Ref SeatsArray array, Const Ref TCanvas canvas) TLayout layout; Begin [= arbitrarily set the header and key region heights =] layout.headheight := 100; [= arbitrary =] layout.keyheight := 60; [= arbitrary - includes date label =] [= plot height is what is left =] layout.plotheight := canvas.height - layout.headheight - layout.keyheight; layout.plotorigin.x := MARGIN; layout.plotorigin.y := layout.headheight + layout.plotheight; [= group width is full available width divided by number of records =] layout.groupwidth := ( canvas.width - 2 * MARGIN ) // ArrayHigh(array, 1); layout.plotwidth := layout.groupwidth * ArrayHigh(array, 1); [= bar width - allow the deduced 'others' =] layout.barwidth := ( layout.groupwidth - 2 * MINBAROFF ) // ( NUMPARTIES + 1 ); [= can no calculate actual offset to centre bars in group =] layout.baroffset := ( layout.groupwidth - layout.barwidth * ( NUMPARTIES + 1 ) ) // 2; [= find the maximum vaule for axis =] layout.maxseats := CalcMaxY(array); [= scaling for seats to pixels =] layout.yscale := layout.plotheight / layout.maxseats; [= work out where to draw the key =] layout.keyorigin.x := layout.plotorigin.x + MARGIN; layout.keyorigin.y := layout.plotorigin.y + 30; [= to allow for dates =] [= if here then ok =] Return layout; End; [= CALC MAX Y ---------- Calculate a rounded up maximum for the 'seats' axis. Assuming 'Other' is never the largest 'party'. =] Private Function Number CalcMaxY(Const Ref SeatsArray array) Number max = 0, quantum = 25; Begin Over array As entry Do [= check all parties and increase max as appropriate =] [= note: whole If - Then can be written on one line =] If entry.lab > max Then max := entry.lab; EndIf; If entry.con > max Then max := entry.con; EndIf; If entry.lib > max Then max := entry.lib; EndIf; EndOver; [= round up to multiple of quantum =] Return Round(max / quantum, Up ) * quantum; End;