[= Demo - Scatter Command: scatter.bat Purpose: =] [= 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, "Cons", Pen { colour -> {0,0,100} } }, { Lib, 2, "Lib", 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} } } ]; [= record to keep all the layout information =] Type TLayout = { [= heading region =] Number headheight, [= plot region =] Point plotorigin, Number plotsize, Number scale, [= key region =] Point keyorigin, Number keyheight, Number maxpercent }; [= PROGRAM ------- The main program reads the data then plots it. =] Program() TVoteArray results; TLayout layout; Number maxpercent; Begin [= read the rew results for the file =] results := ReadData("EXAMPLES:/ukvotes.txt", "EXAMPLES:/fmt1hymd.ini"); [= go through the data to convert to percentages =] If Not PreProcess(results, maxpercent) Then Return; EndIf; [= calculate the layout =] layout := CalcLayout(Canvas, maxpercent); [= draw the main page heading =] Heading(); [= plot the actual data =] Plot(results, 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 TLayout layout) TPen axispen = Pen { colour -> { 70, 70, 70 } }; Point coord; Begin [= plot the X, percentage votes, axis =] VotesAxis(layout, axispen); [= plot the Y, percentage seats, axis =] SeatsAxis(layout, axispen); [= draw the main diagonal =] Line({0, 0}, { ScalePercent(layout.maxpercent, layout), -ScalePercent(layout.maxpercent, layout) }); [= go through the data set =] Over array As item Do [= calculate the coordinates - %votes -> x, %seats -> y =] coord := { ScalePercent(item.votes, layout), -ScalePercent(item.seats, layout) }; Circle(3, Solid) => coord, PartyInfo[item.party].pen; 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 seats v. total votes", 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.plotsize ) // 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; [= SEATS AXIS ---------- The azis numbers uses the default pen. the extra pen is used for the vertical lines. Coordinates relative to plot area. =] Shape SeatsAxis(Const Ref TLayout layout, Const Ref TPen axispen) Point coord; Number percent; Number y; Text label; Begin [= draw the base line with normal pen =] Line( {0,0}, { 0, -ScalePercent(layout.maxpercent, layout) }); [= do the axis title =] TextRotate("Percentage of Seats", 270, Centre, Top) => { -40, -ScalePercent(50, layout)}; For percent From 0 To layout.maxpercent Step 10 Do y := -ScalePercent(percent, layout); If percent <> 0 Then Line( {0, y}, {ScalePercent(layout.maxpercent, layout), y } ) => axispen; EndIf; [= label the value =] label := Format(percent, "I"); TextBlock(label, halign -> Right, valign -> Centre ) => { -4 , y}; EndFor; 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 TLayout layout, Const Ref TPen axispen) Point coord; Number percent; Number x; Text label; Begin [= draw the base line with normal pen =] Line( {0,0}, { ScalePercent(layout.maxpercent, layout), 0 }); [= do the axis title =] TextBlock("Percentage of Votes", halign -> Centre, valign -> Top ) => { ScalePercent(layout.maxpercent / 2, layout), 20}; For percent From 0 To layout.maxpercent Step 10 Do x := ScalePercent(percent, layout); If percent <> 0 Then [= draw the vertical line for the value =] Line( {x, 0}, { x, -ScalePercent(layout.maxpercent, layout) } ) => axispen; EndIf; [= label the value =] label := Format(percent, "I"); TextBlock(label, halign -> Centre, valign -> Top ) => {x, 4}; EndFor; End; [= CALC LAYOUT ----------- Do all the general ayout calculations. =] Function TLayout CalcLayout(Const Ref TCanvas canvas, Number maxpercent) 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.plotsize := canvas.height - layout.headheight - layout.keyheight; layout.plotorigin.y := layout.headheight + layout.plotsize; layout.plotorigin.x := ( canvas.width - layout.plotsize ) // 2; layout.scale := layout.plotsize / maxpercent; layout.keyorigin := { layout.plotorigin.x, canvas.height - layout.keyheight + 40}; layout.maxpercent := maxpercent; [= if here then ok =] Return layout; End; [= FIND TOTALS FOR DATE -------------------- Find the total number of votes and seats for a given date. Actual totals return by reference. =] Function Logical FindTotalsForDate(Time date, Const Ref TVoteArray array, Ref Number votes, Ref Number seats) Begin Over array As item Do [= see if date and 'total' match =] If item.date == date And item.party == Others Then [= found the right item =] votes := item.votes; seats := item.seats; [= no need to look at the rest =] Return True; EndIf; EndOver; [= if not already returned then not found =] votes := Null; seats := Null; Return False; End; [= PRE PROCESS ----------- Go through the data fixing the 'others' votes. =] Function Logical PreProcess(Ref TVoteArray results, Ref Number maxpercent) Number cumvotes = 0; [= cummulative votes for actual parties =] Number cumseats = 0; Number votes, seats, totalvotes, totalseats; Time workdate; [= Null initialisation is ok =] Begin [= clear the maximum percent value =] maxpercent := 0; [= go through the whole table =] Over results As entry Do [= if date changed then a new election =] If workdate <> entry.date Then [= save the new working date =] workdate := entry.date; [= get the totals for the new election =] If Not FindTotalsForDate(workdate, results, totalvotes, totalseats) Then Return False; EndIf; [= reset the accumulators =] cumvotes := 0; cumseats := 0; EndIf; [= the others is a special case - must be last of each set =] If entry.party == Others Then [= need to calculate votes =] votes := entry.votes - cumvotes; seats := entry.seats - cumseats; Else [= copy votes and seats from actual results =] votes := entry.votes; seats := entry.seats; [= add to the cummulative count =] cumvotes += entry.votes; cumseats += entry.seats; EndIf; [= convert results to percentages =] entry.votes := votes / totalvotes * 100; entry.seats := seats / totalseats * 100; If entry.votes > maxpercent Then maxpercent := entry.votes; EndIf; If entry.seats > maxpercent Then maxpercent := entry.seats; EndIf; EndOver; maxpercent := Round(maxpercent / 10, Up ) * 10; Output maxpercent; [= if here then no errors =] Return True; OnError Output "Error: ", Status; [= tell caller there was an error =] Return False; End; [= SCALE PERCENT ------------- Convert pergentage result to pixels. By rounding to nearest pixel we will get a symeteric dot. =] Function Number ScalePercent(Number percent, TLayout layout) Begin [= use the scale deduced earlier =] Return Round(percent * layout.scale); End;