/* In "The Visual Display of Quantitative Information," Edward Tufte reproduces a plot by Charles Minard (1781-1870) and says "it may well be the best statistical graphic ever drawn." The graphic tells the tale of Napoleon's invasion of Russia in June 1812 with 422,000 men. By the time the army reached Moscow in September, the army had shrunk to 100,000 men. As the army retreated back to Poland, bitter cold devastated the survivors. Only 10,000 men returned to Poland. */ run DefineData; declare DataObject dobj; dobj = DataObject.Create( "Napolean", {"Longitude" "Latitude" "ArmySize" "ArmyDir" "ArmyGroup"}, army ); declare ScatterPlot plot; plot = ScatterPlot.Create( dobj, "Longitude", "Latitude", false ); plot.SetWindowPosition( 0, 25, 100, 75 ); plot.SetGraphAreaMargins( 0.08, 0.02, 0.08, 0.15 ); plot.SetAxisViewRange( YAXIS, cityLat[><], cityLat[<>] ); plot.ShowObs( false ); plot.SetTitleText( "Napoleon's Russian Campaign, 1812" ); plot.ShowTitle(); plot.DrawUseDataCoordinates(); sizeScale = 6000; group = unique(armyGroup); do i = ncol(group) to 1 by -1; idx = loc( armyGroup = group[i] ); /* group's advance */ forward = loc( armyDir[idx] = 1 ); plot.DrawSetPenColor( RED ); plot.DrawSetBrushColor( RED ); k = idx[forward]; run PlotContinuousDataPath( armyLon[k], armyLat[k], armySize[k]/sizeScale, plot ); /* group's retreat */ backward = loc( armyDir[idx] = -1 ); plot.DrawSetPenColor( GREY ); plot.DrawSetBrushColor( GREY ); k = idx[backward]; run PlotContinuousDataPath( armyLon[k], armyLat[k], armySize[k]/sizeScale, plot ); end; /* label cities for geographical reference */ plot.DrawSetTextSize( 8 ); plot.DrawSetTextStyle( STYLE_ITALIC ); plot.DrawSetTextAlignment( ALIGN_CENTER, ALIGN_BOTTOM ); plot.DrawText( cityLon, cityLat, cityNames ); /* draw reference axis showing scale in miles */ plot.DrawAxis(33.5, 54.25, 37, 54.25, {0 0.33 0.66 1}, {'0' '25' '50' '75'}, {0.2 0.4 0.6 0.8}, 90 ); plot.DrawSetTextSize( 8 ); plot.DrawText( 35.25, 54.45, "Lieues communes de France" ); /* draw simple legend */ plot.DrawSetTextAlignment( ALIGN_LEFT, ALIGN_CENTER ); plot.DrawSetPenColor( BLACK ); plot.DrawSetBrushColor( RED ); plot.DrawRectangle( 34.6, 54.76, 35, 54.84, true ); plot.DrawText( 35.2, 54.8, "Advance" ); plot.DrawSetBrushColor( GREY ); plot.DrawRectangle( 34.6, 54.66, 35, 54.74, true ); plot.DrawText( 35.2, 54.7, "Retreat" ); /* increase bottom margin and place temperature graphic within */ plot.GetAxisViewRange( YAXIS, ymin, ymax ); plot.SetAxisTickRange( YAXIS, ymin, ymax ); boxMin = ymin - (ymax-ymin)/3; /* decrease by 1/3 */ boxMax = ymin - (ymax-ymin)/20; /* and by 1/20 */ plot.SetAxisViewRange( YAXIS, boxMin, ymax ); plot.GetAxisViewRange( XAXIS, xmin, xmax ); tempMin = -30; tempMax = 0; tempInc = 10; /* draw bounding box and temperature axis */ plot.DrawRectangle( xmin, boxMin, xmax, boxMax ); plot.DrawNumericAxis( xmax, boxMin, xmax, boxMax, tempMin, tempMax, 4 ); /* scale temperature so we can plot in data coordinates */ temp = (retreatTemp - tempMin)/(tempMax-tempMin); temp = temp * ( boxMax - boxMin ) + boxMin; /* draw temperature versus longitude */ plot.DrawSetPenWidth( 2 ); plot.DrawSetPenStyle( DASHED ); plot.DrawLine( retreatLon, temp ); /* label temperature axis */ plot.DrawSetTextAlignment( ALIGN_RIGHT, ALIGN_BOTTOM ); plot.DrawText( xmax, boxMax, "Temp (C)" ); /* label temperature plot with dates of retreat */ k = 1:4; plot.DrawSetTextAlignment( ALIGN_CENTER, ALIGN_TOP ); plot.DrawText( retreatLon[k], temp[k], putn(retreatDate[k],'DATE5.') ); k = 5:nrow(retreatLon); plot.DrawSetTextAlignment( ALIGN_CENTER, ALIGN_BOTTOM ); plot.DrawText( retreatLon[k], temp[k], putn(retreatDate[k],'DATE5.') ); plot.ActivateWindow(); /********************************************/ /* Helper modules */ /********************************************/ start DefineData; GREY = 0808080x; /* city names and locations for annotations */ cityNames = { 'Kowno','Wilna','Smorgoni','Molodexno', 'Gloubokoe','Minsk','Studienska','Polotzk', 'Bobr','Witebsk','Orscha','Mohilow', 'Smolensk','Dorogobouge','Wixma','Chjat', 'Mojaisk','Moscou','Tarantino','Malo-jarosewli'}; cityPos = { 24.0 55.0, 25.3 54.7, 26.4 54.4, 26.8 54.3, 27.7 55.2, 27.6 54.0, 28.5 54.3, 28.7 55.5, 29.4 54.2, 30.2 55.3, 30.4 54.5, 30.4 54.0, 32.0 54.8, 33.2 54.9, 34.3 55.2, 34.4 55.5, 36.0 55.5, 37.6 55.8, 36.6 55.3, 37.3 54.8 }; cityLon = cityPos[,1]; cityLat = cityPos[,2]; /* temperature during retreat */ retreat = { /* lon temp date */ 37.6 0 '18OCT1812'D, 36.0 0 '24OCT1812'D, 33.2 -9 '09NOV1812'D, 32.0 -21 '14NOV1812'D, 29.2 -11 '24NOV1812'D, 28.5 -20 '28NOV1812'D, 27.2 -24 '01DEC1812'D, 26.7 -30 '06DEC1812'D, 25.3 -26 '07DEC1812'D }; retreatLon = retreat[,1]; retreatTemp = retreat[,2]; retreatDate = retreat[,3]; army = { /* dir=1 means 'advancing'; -1 means 'retreating' group identifies separate branches of army movement lon lat survivors dir group */ 24.0 54.9 340000 1 1, 24.5 55.0 340000 1 1, 25.5 54.5 340000 1 1, 26.0 54.7 320000 1 1, 27.0 54.8 300000 1 1, 28.0 54.9 280000 1 1, 28.5 55.0 240000 1 1, 29.0 55.1 210000 1 1, 30.0 55.2 180000 1 1, 30.3 55.3 175000 1 1, 32.0 54.8 145000 1 1, 33.2 54.9 140000 1 1, 34.4 55.5 127100 1 1, 35.5 55.4 100000 1 1, 36.0 55.5 100000 1 1, 37.6 55.8 100000 1 1, 37.7 55.7 100000 -1 1, 37.5 55.67 98000 -1 1, 37.0 55.0 97000 -1 1, 36.8 55.0 96000 -1 1, 35.4 55.3 87000 -1 1, 34.3 55.2 55000 -1 1, 33.3 54.8 37000 -1 1, 32.0 54.6 24000 -1 1, 30.4 54.4 20000 -1 1, 29.2 54.3 20000 -1 1, 29.13 54.29 50000 -1 1, /* joined by group 2 */ 28.5 54.2 50000 -1 1, 28.3 54.3 48000 -1 1, 26.8 54.3 12000 -1 1, 26.8 54.4 14000 -1 1, 25.0 54.4 8000 -1 1, 24.4 54.4 4000 -1 1, 24.2 54.4 4000 -1 1, 24.1 54.4 4000 -1 1, /* Group 2 */ 24.0 55.1 60000 1 2, 24.5 55.2 60000 1 2, 25.5 54.7 60000 1 2, 26.6 55.7 40000 1 2, 27.4 55.6 33000 1 2, 28.7 55.5 33000 1 2, 28.7 55.5 33000 -1 2, 29.2 54.29 30000 -1 2, /* 28.5 54.1 30000 -1 2, 28.3 54.2 28000 -1 2, */ /* Group 3 */ 24.0 55.2 22000 1 3, 24.5 55.3 22000 1 3, 24.6 55.8 6000 1 3, 24.6 55.8 6000 -1 3, 24.2 54.4 6000 -1 3, 24.1 54.4 6000 -1 3 }; armyLon = army[,1]; armyLat = army[,2]; armySize= army[,3]; armyDir = army[,4]; armyGroup=army[,5]; finish; /* This module s only valid for plots with two interval axes. mX and mY are vertices for a PW line in data coordinates mSize is the width in pixels at each vertex The module attempts to plot the data as a path whose width varies continuously from vertex to vertex. SIDE EFFECT: plot is left in data coordinates. */ start PlotContinuousDataPath( mX, mY, mSize, Plot plot ); x = rowvec( mX ); y = rowvec( mY ); size = rowvec( mSize ); n = ncol(x); if n < 2 then do; print "GetContinuousPath: path must contain at least two vertices"; abort; end; if n ^= ncol(y) | n ^= ncol(size) then do; print "GetContinuousPath: parameter arrays are different lengths"; abort; end; /* translate all coordinates into pixel coords */ plot.GetGraphAreaWidthHeight( winWidth, winHeight ); /* find pixel coordinates of frame */ plot.GetGraphAreaMargins( leftGraphMargin, rightGraphMargin, topGraphMargin, bottomGraphMargin ); plotHeight = (1 - topGraphMargin - bottomGraphMargin) * winHeight; plotWidth = (1 - leftGraphMargin - rightGraphMargin) * winWidth; plotLeft = leftGraphMargin * winWidth; plotRight = plotLeft + plotWidth; plotBottom = bottomGraphMargin * winHeight; plotTop = plotBottom + plotHeight; /* find pixel coordinates of plot area (includes plot margins) */ plot.GetPlotAreaMargins( leftPlotMargin, rightPlotMargin, topPlotMargin, bottomPlotMargin ); drawHeight = (1 - topPlotMargin - bottomPlotMargin) * plotHeight; drawWidth = (1 - leftPlotMargin - rightPlotMargin) * plotWidth; drawLeft = plotLeft + leftPlotMargin * plotWidth; drawRight = drawLeft + drawWidth; drawBottom = plotBottom + bottomPlotMargin * plotHeight; drawTop = drawBottom + drawHeight; run GetPlotAreaDataCoordinates( plotXMin, plotXMax, plotYMin, plotYMax, plot ); /* translate data into [plot area] pixels */ x = plotLeft + (plotRight-plotLeft)/(plotXMax-plotXMin) * (x - plotXMin); y = plotBottom + (plotTop-plotBottom)/(plotYMax-plotYMin) * (y - plotYMin); path = j(2*n, 2, .); /* find points that define width of first line segment */ u = y[1]-y[2] || x[2]-x[1]; uNorm = u / sqrt(ssq(u)); /* unit length */ wx = size[1]/2 * uNorm[1]; wy = size[1]/2 * uNorm[2]; path[1,1] = x[1] + wx; path[1,2] = y[1] + wy; path[2*n,1] = x[1] - wx; path[2*n,2] = y[1] - wy; /* define width of intermediate points. We average the directions perpendicular to each line segment */ do i = 2 to n-1; u = y[i-1]-y[i] || x[i]-x[i-1]; uNorm = u / sqrt(ssq(u)); /* unit length */ v = y[i]-y[i+1] || x[i+1]-x[i]; vNorm = v / sqrt(ssq(v)); /* unit length */ u = (uNorm + vNorm)/2; /* average directions */ uNorm = u / sqrt(ssq(u)); wx = size[i]/2 * uNorm[1]; wy = size[i]/2 * uNorm[2]; path[i,1] = x[i] + wx; path[i,2] = y[i] + wy; path[2*n+1-i,1] = x[i] - wx; path[2*n+1-i,2] = y[i] - wy; end; /* find points that define width of last line segment */ u = y[n-1]-y[n] || x[n]-x[n-1]; uNorm = u / sqrt(ssq(u)); /* unit length */ wx = size[n]/2 * uNorm[1]; wy = size[n]/2 * uNorm[2]; path[n,1] = x[n] + wx; path[n,2] = y[n] + wy; path[n+1,1] = x[n] - wx; path[n+1,2] = y[n] - wy; /* now convert back to data coordinates */ path[,1] = plotXMin + (plotXMax-plotXMin)/(plotRight-plotLeft) * (path[,1] - plotLeft); path[,2] = plotYMin + (plotYMax-plotYMin)/(plotTop-plotBottom) * (path[,2] - plotBottom); plot.DrawUseDataCoordinates(); plot.DrawPolygon( path[,1], path[,2], true ); finish;