[= COVID ----- This is a program to display data for the 2020 Covid-19 epidemic in the United Kingdon. It can be adapted to other countries by changing title strings and renaming the data file. =] [= program parameter enumerated types =] Private Type EMetric = ( Cases, Deaths ); [= the metric to be displayed =] Private Type ESelect = ( Total, New, Diff, Average ); [= which cases to select =] Private Type EPeriod = ( Day, Week ); [= period of data accumutation =] [= The scale mode is not currently used =] Private Type EScale = ( Linear, Log ); [= how to scale the value axis =] [= raw data structure =] Private Type TValueArray = Number[]; [= open array for the actual value list =] Private Type TRawRecord = { Time date, Text action, TValueArray values }; Private Type TRawArray = TRawRecord[]; [= display data structure =] Private Type TDispRecord = { Number day, Number value }; Private Type TDispArray = TDispRecord[]; Private Type TColourArray = Colour[1 To 2]; [= start date - fixed for now - data must start on this date =] Const Time StartDate = '2020-01-01'; Time EndDate; Const TColourArray BarColour = [ {0, 0, 100}, {100, 0, 0} ]; [= Layout - all plots use the same basic layout so make it global =] Number LeftMargin, RightMargin; Number LabelWidth; Number HeaderHeight; Number FooterHeight; Number PlotHeight, PlotWidth; Number PlotXOrigin, PlotYOrigin; Number TimeTick = 10; [= length of mark on axis =] [= Axes - axis scaling data etc. - make global =] Number MaxDays; Number DayScale; Logical ValueAxisBipolar = False; Number ValueAxisTick = 10; Number ValueAxisPower; Number ValueAxisTicks; Number ValueAxisZero = 0; Number ValueAxisMax; Number ValueAxisScale; [= PROGRAM ------- =] Program(EMetric metric = Cases, ESelect select = New, EPeriod period = Day, EScale scale = Linear) TRawArray rawdata; [= raw data from file =] TDispArray dispdata; [= processes data to display =] Number index; [= index into value array =] Number maxvalue; [= maximum value of the display data =] Text subtitle = ""; Logical ok; Begin [= read in the raw data file =] rawdata := ReadData("covid_uk.txt", "covidfmt.txt"); [= need to convert metric enum to an array index =] index := ValueIndex(metric); subtitle := Format(metric, "T"); [= build the correct data set for display =] Select select From Case Total Do [= total only done for day data =] dispdata := BuildTotalDay(rawdata, index); subtitle := "Total " + Format(metric, "T"); Case New Do [= new can be done by day or week =] Select period From Case Day Do dispdata := BuildNewDay(rawdata, index); subtitle := "Daily " + Format(metric, "T"); Case Week Do dispdata := BuildNewWeek(rawdata, index); subtitle := "Weekly " + Format(metric, "T"); EndSelect; Case Diff Do dispdata := BuildDiff(rawdata, index, period); subtitle := "Per " + Format(period, "T") + " Differences for " + Format(metric, "T"); [= rates can be negative =] ValueAxisBipolar := True; Case Average Do dispdata := BuildAverage(rawdata, index); subtitle := "Last 7 day average " + Format(metric, "T"); EndSelect; Title("Covid-19 - United Kingdom: " + subtitle); [= set up the display area =] ok := SetupLayout(Canvas); ok := SetupAxes(rawdata, dispdata); With { PlotXOrigin, PlotYOrigin } Do ValuePlot(dispdata, period, scale, BarColour[index]); [= draw the time axis - same for all plots =] TimeAxis(); ValueAxis(); EndWith; End; [= SHAPES =] [= ------ =] [= VALUE PLOT ---------- =] Shape ValuePlot(Const Ref TDispArray dispdata, EPeriod period, EScale scale, Colour barclr) Number ypixels, width; TPen barpen = Pen { colour -> barclr }; Begin [= see how wide to make the bars =] If period == Week Then width := 7; Else width := 1; EndIf; [= go through the data =] Over dispdata As row Do [= do not plot null values =] If row.value <> Null Then [= scale the data =] ypixels := ScaleValue(row.value, scale); Rectangle(DayScale * width, -ypixels, Solid) => { row.day * DayScale, -ValueAxisZero}, barpen; EndIf; EndOver; End; [= TIME AXIS --------- This will generate the time axis. It will draw a sent of complte months. =] Shape TimeAxis() Number firstmonth, lastmonth, absmonth, relmonth, days, calmonth, calyear; Time monthstart; Text label; Begin [= draw the main axis line =] LineTo( {MaxDays * DayScale, 0} ); [= calculate absolute month numbers =] firstmonth := AbsMonthCalc(StartDate); lastmonth := AbsMonthCalc(EndDate); For absmonth From firstmonth To lastmonth Do relmonth := absmonth - firstmonth; [= convert to calendar dates for display =] monthstart := AbsMonthSplit(absmonth, calyear, calmonth); [= calculte tick position =] days := DiffDays(StartDate, monthstart); [= draw the tick =] LineTo( {0, TimeTick} ) => { days * DayScale, 0}; LineTo( {0, -PlotHeight} ) => { days * DayScale, 0}; [= for all but 'final' month =] If absmonth < lastmonth Then label := Format(monthstart, "N"); [= for January add the year =] If calmonth == 1 Then label += " " + Format(monthstart, "Y4"); EndIf; TextBlock(label, halign -> Left, valign -> Top) => { days * DayScale + 2, 2}; EndIf; EndFor; End; [= TITLE ----- =] Shape Title(Text title) TFont titlefont = Font { size -> 24 }; Begin TextBlock(title, halign -> Centre, valign -> Top) => { Canvas.width // 2, 14}, titlefont; End; [= VALUE AXIS ---------- =] Shape ValueAxis() Number tick; Number ypixels; Text label; Begin For tick From 0 To ValueAxisTicks Do [= scale the data =] ypixels := -ScaleValue(tick * ValueAxisPower, Linear); Line( {-ValueAxisTick, 0 }, {PlotWidth, 0}) => {0, ypixels - ValueAxisZero}; label := Format(tick * ValueAxisPower , "It"); TextBlock(label, halign -> Right, valign -> Centre) => {-ValueAxisTick, ypixels - ValueAxisZero}; If ValueAxisBipolar Then Line( {-ValueAxisTick, 0 }, {PlotWidth, 0}) => {0, -ypixels - ValueAxisZero}; label := Format(-tick * ValueAxisPower , "It"); TextBlock(label, halign -> Right, valign -> Centre) => {-ValueAxisTick, -ypixels - ValueAxisZero}; EndIf; EndFor; End; [= FUNCTIONS =] [= --------- =] [= ABS MONTH CALC -------------- Calculate absolute month number. =] Function Number AbsMonthCalc(Time date) Begin Return GetYear(date) * 12 + GetMonth(date) - 1; End; [= ABS MONTH SPLIT --------------- =] Function Time AbsMonthSplit(Number absmonth, Ref Number calyear, Ref Number calmonth) Begin calyear := absmonth // 12; calmonth := absmonth Mod 12 + 1; [= also return as a date =] Return MakeDate(calyear, calmonth, 1); End; [= BUILD DIFF ---------- Build the growth rate array. =] Private Function TDispArray BuildDiff(Const Ref TRawArray rawdata, Number imetric, EPeriod period) TDispArray dispdata; Number ivalue, high, prevcount, count, diff; Begin [= build the change array =] Select period From Case Day Do dispdata := BuildNewDay(rawdata, imetric); Case Week Do dispdata := BuildNewWeek(rawdata, imetric); EndSelect; [= only do if at least two points =] high := ArrayHigh(dispdata, 1); If high < 2 Then Return Null; EndIf; [= initialise =] prevcount := dispdata[1].value; dispdata[1].value := Null; [= go through rest of data =] For ivalue From 2 To high Do [= get the current count =] count := dispdata[ivalue].value; diff := count - prevcount; [= save the value =] prevcount := count; [= overwrire data array with the ratio =] dispdata[ivalue].value := diff; EndFor; Return dispdata; End; [= BUILD AVERAGE ------------- Build a last seven day average. =] Private Function TDispArray BuildAverage(Const Ref TRawArray rawdata, Number index) TDispArray newdata; Number totalrows, row; Begin [= save the total number of rows =] totalrows := ArrayHigh(rawdata, 1); [= create new array with size of original =] newdata := Array(totalrows); [= set first seven days to 0 =] For row From 1 To 7 Do newdata[row].day := DiffDays(StartDate, rawdata[row].date); newdata[row].value := 0; EndFor; For row From 8 To totalrows Do newdata[row].day := DiffDays(StartDate, rawdata[row].date); newdata[row].value := ( rawdata[row].values[index] - rawdata[row - 7].values[index] ) / 7; EndFor; [= return the new array =] Return newdata; End; [= BUILD NEW DAY ------------- Build the per day new data. =] Private Function TDispArray BuildNewDay(Const Ref TRawArray rawdata, Number index) TDispArray newdata; Number prevcount = 0; Number totalrows, row; Begin [= save the total number of rows =] totalrows := ArrayHigh(rawdata, 1); [= create new array with size of original =] newdata := Array(totalrows); [= go through calculating the differences =] For row From 1 To ArrayHigh(rawdata, 1) Do [= day number starting at 0 =] newdata[row].day := DiffDays(StartDate, rawdata[row].date); [= new cases is current minus previous =] newdata[row].value := rawdata[row].values[index] - prevcount; prevcount := rawdata[row].values[index]; EndFor; [= return the new array =] Return newdata; End; [= BUILD NEW WEEK -------------- Build the per week new data. =] Private Function TDispArray BuildNewWeek(Const Ref TRawArray rawdata, Number index) Number days; [= tatal number of days =] Number skip; [= points at the beginning to skip =] Number week, weeks; [= number of full weeks =] Number prevcount = 0; Number weekstart; [= index of first value or the week =] Number endcount; [= count at end of the week =] TDispArray dispdata; Begin days := ArrayHigh(rawdata, 1); [= check at least one week of data =] If days < 7 Then Return Null; EndIf; [= calculate counts =] weeks := days // 7; skip := days Mod 7; [= allocate the result array =] dispdata := Array(weeks); [= go through all the weeks =] For week From 1 To weeks Do weekstart := 7 * ( week - 1 ) + skip; [= get the start date for the week =] dispdata[week].day := DiffDays(StartDate, rawdata[weekstart + 1].date); endcount := rawdata[weekstart + 7].values[index]; dispdata[week].value := endcount - prevcount; [= save the end count =] prevcount := endcount; EndFor; Return dispdata; End; [= BUILD TOTAL DAY --------------- Build the total data. Just copy the index raw field. =] Private Function TDispArray BuildTotalDay(Const Ref TRawArray rawdata, Number index) TDispArray dispdata; Number row, count; Begin [= get the count for the raw data =] count := ArrayHigh(rawdata, 1); [= create a display data array the same ize =] dispdata := Array(count); [= go through the array fetching the data =] For row From 1 To count Do [= convert date to a day from the start =] dispdata[row].day := DiffDays(StartDate, rawdata[row].date); [= copy the value as is =] dispdata[row].value := rawdata[row].values[index]; EndFor; [= return the new array =] Return dispdata; End; [= CALC MAX DAYS ------------- This will set the end day to then end of the month for the last dsata point and use that to determine the time axis extents. =] Function Number CalcMaxDays(Const Ref TRawArray rawdata) Number year, month; Time date; Number lastindex; Begin [= sort out the end of the axis =] lastindex := ArrayHigh(rawdata, 1); date := rawdata[lastindex].date; [= adjust to start of following month =] year := GetYear(date); month := GetMonth(date); [= go to start of following month =] If month == 12 Then month := 1; year += 1; Else month += 1; EndIf; EndDate := MakeDate(year, month, 1); [= return as data =] Return DiffDays(StartDate, EndDate); End; [= CALC VALUE AXIS --------------- Calculate the parameters for the value (Y) axis. =] Function Number CalcValueAxis(Number maxvalue, Ref Number ticks, Ref Number power) Begin [= ensure not zero or null =] If maxvalue == Null Or maxvalue == 0 Then maxvalue := 1; EndIf; power := 1; ticks := maxvalue; While maxvalue > 15 Do power *= 10; maxvalue := maxvalue // 10; EndWhile; [= round up a bit =] ticks := maxvalue + 1; Return ticks * power; End; [= MAX VALUE --------- Find the maximum value of the display data. Assumes all data is positive. =] Function Number MaxValue(Const Ref TDispArray dispdata) Number max = 0; Begin Over dispdata As row Do If row.value <> Null And Abs(row.value) > max Then max := Abs(row.value); EndIf; EndOver; Return max; End; [= SCALE VALUE ----------- =] Function Number ScaleValue(Number value, EScale scale) Begin [= should not happen =] If value == Null Then Return Null; EndIf; If scale == Log Then value := Log10(value); EndIf; Return value * ValueAxisScale; End; [= SETUP AXES ---------- =] Function Logical SetupAxes(Const Ref TRawArray rawdata, Const Ref TDispArray dispdata) Number maxvalue; [= the actual maximum value =] Begin MaxDays := CalcMaxDays(rawdata); DayScale := PlotWidth / MaxDays; maxvalue := MaxValue(dispdata); ValueAxisMax := CalcValueAxis(maxvalue, ValueAxisTicks, ValueAxisPower); [= for linear scale =] ValueAxisScale := PlotHeight / ValueAxisMax; If ValueAxisBipolar Then ValueAxisZero := PlotHeight / 2; ValueAxisScale /= 2; EndIf; Return True; End; [= SETUP LAYOUT ------------ Thi could be tidied up. =] Function Logical SetupLayout(TCanvas canvas) Begin [= the following are fudged for now =] LeftMargin := 40; RightMargin := 40; LabelWidth := 40; HeaderHeight := 60; FooterHeight := 40; PlotWidth := canvas.width - LeftMargin - LabelWidth - RightMargin; PlotHeight := canvas.height - HeaderHeight - FooterHeight; PlotXOrigin := LeftMargin + LabelWidth; PlotYOrigin := HeaderHeight + PlotHeight; Return True; End; [= VALUE INDEX ----------- =] Private Function Number ValueIndex(EMetric metric) Begin [= select base on the enum value =] Select metric From Case Cases Do Return 1; Case Deaths Do Return 2; EndSelect; [= should never get here =] Return Null; End;