[= MULTI COV --------- This is a multi-region version of the Covid-19 virus data dispaly system. It will plot several regions on the same graph. All data will be scaled to the each region's population. =] [= limit number of plots because of colours =] Private Const Number MaxPlots = 6; Private Type EMetric = ( Cases, Deaths ); [= the metric to be displayed =] Private Type ESelect = ( Day, Total ); [= which period select =] [= 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[]; [= ini file structure =] Private Type TIniRecord = { Text code, Text region, Text extcounty, Text extregion, Number population}; Private Type TIniArray = TIniRecord[]; [= display data structure =] Private Type TDispRecord = { Number day, Number value }; Private Type TDispArray = TDispRecord[]; Private Type TPlotRecord = { TIniRecord inidata, TDispArray dispdata}; Private Type TPlotArray = TPlotRecord[]; Private Type TColourArray = Colour[]; [= start date - fixed for now - data must start on this date =] Const Time StartDate = '2020-01-01'; Time EndDate; Const TColourArray LineColour = [ {100, 0, 0}, {0, 0, 100}, {0, 100, 0}, {100, 0, 100}, {0, 100, 100}, {80,80,0} ]; Const TColourArray TitleColour = [{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 = 0; 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 = Day) TIniArray inifile; [= config file =] TRawArray rawdata; [= raw data from file =] TPlotArray plotdata; [= complete set of data to plot =] Number region, regions; [= total number of regions =] Text datafile; [= name for the region's data file =] Number index; [= index into value array =] Logical ok; Number max = 0, newmax; Text title; Begin [= read in the ini file =] inifile := ReadData("covdata.ini"); [= allocate the PlotArray =] regions := ArrayHigh(inifile, 1); If regions > MaxPlots Then regions := MaxPlots; EndIf; plotdata := Array(regions); [= need to convert metric enum to an array index =] index := ValueIndex(metric); [= go through all the regions =] For region From 1 To regions Do [= copy the ini file data =] plotdata[region].inidata := inifile[region]; [= read in the raw data for the region =] datafile := "covid_" + inifile[region].code + ".txt"; rawdata := ReadData(datafile, "covidfmt.txt"); newmax := CalcMaxDays(rawdata); If newmax > MaxDays Then MaxDays := newmax; EndIf; Select select From Case Day Do plotdata[region].dispdata := BuildDay(rawdata, index); title := "Comparative Averaged Daily " + Format(metric, "T"); Case Total Do plotdata[region].dispdata := BuildTotal(rawdata, index); title := "Comparative Total " + Format(metric, "T"); EndSelect; [= scale by polulation =] newmax := ScaleData(plotdata[region].dispdata, inifile[region].population); [= see if more than previous max =] If newmax > max Then max := newmax; EndIf; EndFor; [= set up the display area =] ok := SetupLayout(Canvas); ok := SetupAxes(max); Title("Covid-19 " + title) => Pen { colour -> TitleColour[index] }; With { PlotXOrigin, PlotYOrigin } Do [= reverse order ensures higher in list on to =] For region From regions To 1 Step -1 Do ValuePlot(plotdata[region].dispdata, LineColour[region]) => Pen { width -> 2 }; EndFor; [= draw the time axis - same for all plots =] TimeAxis(); ValueAxis(); [= colour key for data plots =] Key(inifile, regions) => Pen { width -> 2 }; EndWith; End; [= ------ =] [= SHAPES =] [= ------ =] [= KEY --- =] Shape Key(Const Ref TIniArray ini, Number count) Const Number linelen = 20; Number i, width; Begin [= sort out the width base on max =] width := PlotWidth // MaxPlots; For i From 1 To count Do With { ( i - 1 ) * width , 40 } Do [= draw the sample line =] Line( {0, 0}, {linelen, 0} ) => Pen { colour -> LineColour[i] }; TextBlock(ini[i].region, valign -> Centre ) => { linelen + 4, 0 }; EndWith; 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); 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; [= position slightly fudged =] TextRotate("Per Million of Population", -90, halign -> Centre) => { -(LabelWidth+20), -PlotHeight // 2}; End; [= VALUE PLOT ---------- =] Shape ValuePlot(Const Ref TDispArray dispdata, Colour colour) Number xpixels, prevx, ypixels, prevy; Number row; TPen linepen = Pen { colour -> colour }; Begin [= set up on the fists =] prevx := dispdata[1].day * DayScale; prevy := ScaleValue(dispdata[1].value); [= go through the data =] For row From 2 To ArrayHigh(dispdata, 1) Do [= do not plot null values =] If dispdata[row].value <> Null Then [= scale the data =] xpixels := dispdata[row].day * DayScale; ypixels := ScaleValue(dispdata[row].value); Dot() => { xpixels, -ypixels}; Line( {prevx, -prevy}, {xpixels, -ypixels} ) => linepen; prevx := xpixels; prevy := ypixels; EndIf; EndFor; 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; [= --------- =] [= 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 DAY --------- Build a last seven day average. =] Private Function TDispArray BuildDay(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 TOTAL ----------- Build the output array for the total plot. =] Private Function TDispArray BuildTotal(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); For row From 1 To totalrows Do newdata[row].day := DiffDays(StartDate, rawdata[row].date); [= use the raw data =] newdata[row].value := rawdata[row].values[index]; EndFor; [= return the new array =] Return newdata; 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; [= ensure mintiple of ticks - will round down =] PlotHeight := ( PlotHeight // ticks ) * ticks; Return ticks * power; End; [= SCALE DATA ---------- Scale all the values by the population in millions. Will also return maximum value. =] Private Function Number ScaleData(Ref TDispArray dispdata, Number pop) Number max = 0; Begin [= get population as millions =] pop /= 1000000; Over dispdata As entry Do entry.value /= pop; If entry.value > max Then max := entry.value; EndIf; EndOver; Return max; End; [= SCALE VALUE ----------- =] Function Number ScaleValue(Number value) Begin [= should not happen =] If value == Null Then Return Null; EndIf; Return value * ValueAxisScale; End; [= SETUP AXES ---------- =] Function Logical SetupAxes(Number maxvalue) Begin DayScale := PlotWidth / MaxDays; maxvalue := Round(maxvalue, Up); ValueAxisMax := CalcValueAxis(maxvalue, ValueAxisTicks, ValueAxisPower); [= for linear scale =] ValueAxisScale := PlotHeight / ValueAxisMax; 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 := 80; 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;