[= Demo - Mulitple Lines Command: multiline.bat Purpose: =] [= Global layout values =] Private Const Number MARGIN = 40; [= margin to left and right of plot =] Private Const Number NUMPARTIES = 5; [= number of actual quoted parties =] Private Const Number MINBAROFF = 10; [= minimum bar offset ( create gap between groups ) =] [= types for the data =] [= enumerated type for the parties ( plus Total ) =] Type EParty = ( Lab, Con, Lib, Green, UKIP, Others = "Total" ); [= structure type for the raw results =] Type TVoteRecord = { Time date, EParty party, Number votes, Number seats }; [= tpye for the raw result table =] Type TVoteArray = TVoteRecord[]; [= type for the party information =] Type TPartyInfo = { EParty party, Number index, Text name, TPen pen }; [= array of party information structure index on Party enum =] Type TPartyArray = TPartyInfo[EParty]; [= special array to hold coords of previous point for line drawing =] Type TPrevCoords = Point[EParty]; Private TPartyArray PartyInfo = [ { Lab, 0, "Labour", Pen { colour -> {100,0,0} } }, { Con, 1, "Conservative", Pen { colour -> {0,0,100} } }, { Lib, 2, "Liberal/LibDem", Pen { colour -> {100,70,0} } }, { Green, 3, "Green", Pen { colour -> {0,100,0} } }, { UKIP, 4, "UKIP", Pen { colour -> {100, 0,100} } }, { Others, 5, "Others", Pen { colour -> {50, 50,50} } } ]; [= a structure for the vsarious data limits =] Type TLimits = { Logical ok = False, Time lowdate, Time highdate, Number maxvotes = 0 }; [= record to keep all the layout information =] Type TLayout = { [= heading region =] Number headheight, [= plot region =] Point plotorigin, Number plotwidth, Number plotheight, Time datestart, Number datescale, Number votescale, [= key region =] Point keyorigin, Number keyheight }; [= PROGRAM ------- The main program reads the data then plots it. =] Program() TVoteArray results; TLimits limits; TLayout layout; Begin [= read the raw results for the file =] results := ReadData("EXAMPLES:/ukvotes.txt", "EXAMPLES:/fmt1hymd.ini"); [= go through the data to correct 'others' and find limits =] limits := PreProcess(results); If Not limits.ok Then Return; EndIf; [= calculate the layout =] layout := CalcLayout(results, Canvas, limits); [= draw the main page heading =] Heading(); [= plot the actual data =] Plot(results, limits, layout) => layout.plotorigin; [= draw the colour key at thebottom =] Key(layout) => layout.keyorigin; OnError [= we really should not get here =] Output "**** unexpected error"; Output Status; End; [= PLOT ---- This will plot the actual data. The date-dependent labels for the X axis are also writtn here. =] Shape Plot( Const Ref TVoteArray array, Const Ref TLimits limits, Const Ref TLayout layout) TPrevCoords prevcoords; Time prevdate; [= defaults to Null which is just right =] Point coord; TPen axispen = Pen { colour -> { 70, 70, 70 } }; Begin [= plot the Y axis =] VotesAxis(limits, layout, axispen); [= go through the data set =] Over array As item Do [= see if first for this date =] If item.date <> prevdate Then [= draw the verival line and label it =] coord := { ScaleDate( item.date, layout), 0 }; Line(coord, {coord.x, coord.y - layout.plotheight} ) => axispen; [= write the month =] TextBlock(Format(item.date, "N3"), halign-> Centre ) => coord; [= and the year =] coord.y += 16; TextBlock(Format(item.date, "Y4"), halign-> Centre ) => coord; [= rememer this date =] prevdate := item.date; EndIf; [= use the party pen for line and marker =] With PartyInfo[item.party].pen Do [= work out where to plot the point =] coord := { ScaleDate( item.date, layout), -ScaleVotes(item.votes, layout) }; [= if a valid previous point =] If prevcoords[item.party].x <> Null Then [= draw line fro previous result =] Line(prevcoords[item.party], coord); EndIf; [= mark the point with a solid circle =] Circle(3, Solid) => coord, PartyInfo[item.party].pen; EndWith; [= save the coordinate of this point =] prevcoords[item.party] := coord; 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("Total votes won by major parties", halign -> Centre, valign -> Top) => { Canvas.width // 2, 50 }, subfont; End; [= KEY --- Write the colour key for the parties. =] Shape Key(TLayout layout) Number width = ( layout.plotwidth ) // 6; Number radius = 5; Begin [= go through the array of party details =] Over PartyInfo As item Do [= shift both the mark and the label together =] With { width * item.index + radius, radius } Do [= draw a circle in the party colour =] Circle(radius, Solid) => item.pen; [= write the party name =] TextBlock(item.name, valign -> Centre) => {radius + 10, 0}; EndWith; EndOver; End; [= VOTES AXIS ---------- The azis numbers us the default pen. the exta pen is used for the horizontal lines. Coordinates relative to plot area. =] Shape VotesAxis(Const Ref TLimits limits, Const Ref TLayout layout, Const Ref TPen axispen) Number votes; Point coords; Text label; Begin [= put a scale heading at the top of the axis =] TextBlock("Million", halign -> Right, valign -> Bottom) => {0, -layout.plotheight}; [= mark every million votes - last will be not match end point =] For votes From 0 To limits.maxvotes Step 1000000 Do [= where the line starts =] coords := { 0, -ScaleVotes(votes, layout) }; [= draw the line with the passed axis pen =] Line(coords, { layout.plotwidth, coords.y }) => axispen; label := Format(votes / 1000000, "I"); [= write the text with the default pen =] TextBlock(label, halign -> Right, valign -> Centre) => coords; EndFor; End; [= CALC LAYOUT ----------- Do all the general ayout calculations. =] Function TLayout CalcLayout(Const Ref TVoteArray array, Const Ref TCanvas canvas, TLimits limits) TLayout layout; Time enddate; Number totaldays; Begin [= arbitrarily set the header and key region heights =] layout.headheight := 100; [= arbitrary =] layout.keyheight := 100; [= arbitrary - includes date labels =] [= 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; layout.plotwidth := canvas.width - 2 * MARGIN; layout.keyorigin := { MARGIN, canvas.height - layout.keyheight + 40}; [= start at 1st Jan first year =] layout.datestart := MakeDate(GetYear(limits.lowdate), 1, 1); [= end 31st of last year =] enddate := MakeDate(GetYear(limits.highdate), 12, 31); totaldays := DiffDays(layout.datestart, enddate) + 1; layout.datescale := layout.plotwidth / totaldays; [= pixels per 1000 votes =] layout.votescale := layout.plotheight / (limits.maxvotes / 1000 ); [= if here then ok =] Return layout; End; [= PRE PROCESS ----------- Go through the data fixing the 'others' votes. =] Function TLimits PreProcess(Ref TVoteArray results) TLimits limits; Number cumvote = 0; [= cummulative votes for actual parties =] Begin [= part defaults, part from 1st element in array =] limits := { lowdate -> results[1].date, highdate -> results[1].date }; [= go through the whole table =] Over results As entry Do [= the others is a special case - must be last of each set =] If entry.party == Others Then [= correct the 'others' votes =] entry.votes := entry.votes - cumvote; [= reset the cummulative votes =] cumvote := 0; [= check the date =] If entry.date > limits.highdate Then [= save as new high date =] limits.highdate := entry.date; EndIf; Else [= add to the cummulative count =] cumvote += entry.votes; EndIf; [= see if need to update maximum - this will include 'others' =] If entry.votes > limits.maxvotes Then limits.maxvotes := entry.votes; EndIf; EndOver; [= if here then no errors =] limits.ok := True; Return limits; OnError Output "Error: ", Status; [= the ok should still be False form initialisation but play safe =] limits.ok := False; Return limits; End; [= SCALE DATE ---------- Convert a date into a pixel offset. =] Function Number ScaleDate(Time date, TLayout layout) Begin [= use the day count from start =] Return DiffDays(layout.datestart, date) * layout.datescale; End; [= SCALE VOTES ----------- Convert number of votes to height in pixels. =] Function Number ScaleVotes(Number votes, TLayout layout) Begin [= use the scale deduced earlier =] Return votes / 1000 * layout.votescale; End;