------------------------------------------------------------------------------- |-- Module : Graphics.Rendering.Chart.Axis.Floating-- Copyright : (c) Tim Docker 2010-- License : BSD-style (see chart/COPYRIGHT)---- Calculate and render floating value axes-- including doubles with linear, log, and percentage scaling.--{-# LANGUAGE GeneralizedNewtypeDeriving #-}{-# OPTIONS_GHC -XTemplateHaskell #-}moduleGraphics.Rendering.Chart.Axis.Floating(Percent(..),LinearAxisParams(..),LogValue(..),LogAxisParams(..),defaultLinearAxis,defaultLogAxis,autoScaledAxis,autoScaledLogAxis,autoSteps,la_labelf,la_nLabels,la_nTicks,loga_labelf)whereimportData.List(minimumBy)importData.Ord(comparing)importNumeric(showFFloat)importData.Accessor.TemplateimportGraphics.Rendering.Chart.TypesimportGraphics.Rendering.Chart.Axis.TypesinstancePlotValueDoublewheretoValue=idfromValue=idautoAxis=autoScaledAxisdefaultLinearAxis-- | A wrapper class for doubles used to indicate they are to-- be plotted against a percentage axis.newtypePercent=Percent{unPercent::Double}deriving(Eq,Ord,Num,Real,Fractional,RealFrac,Floating,RealFloat)instanceShowPercentwhereshow(Percentd)=showD(d*100)++"%"instancePlotValuePercentwheretoValue=unPercentfromValue=PercentautoAxis=autoScaledAxisdefaultLinearAxis{-la_labelf_=-}-- | A wrapper class for doubles used to indicate they are to-- be plotted against a log axis.newtypeLogValue=LogValueDoublederiving(Eq,Ord,Num,Real,Fractional,RealFrac,Floating,RealFloat)instanceShowLogValuewhereshow(LogValuex)=showxinstancePlotValueLogValuewheretoValue(LogValuex)=logxfromValued=LogValue(expd)autoAxis=autoScaledLogAxisdefaultLogAxisshowD::(RealFloatd)=>d->StringshowDx=casereverse$showFFloatNothingx""of'0':'.':r->reverserr->reverserdataLinearAxisParamsa=LinearAxisParams{-- | The function used to show the axes labels.la_labelf_::a->String,-- | The target number of labels to be shown.la_nLabels_::Int,-- | The target number of ticks to be shown.la_nTicks_::Int}defaultLinearAxis::(Showa,RealFloata)=>LinearAxisParamsadefaultLinearAxis=LinearAxisParams{la_labelf_=showD,la_nLabels_=5,la_nTicks_=50}-- | Generate a linear axis automatically, scaled appropriately for the-- input data.autoScaledAxis::RealFloata=>LinearAxisParamsa->AxisFnaautoScaledAxislapps0=makeAxis'realToFracrealToFrac(la_labelf_lap)(labelvs,tickvs,gridvs)whereps=filterisValidNumberps0(min,max)=(minimumps,maximumps)range[]=(0,1)range_|min==max=ifmin==0then(-1,1)elseletd=abs(min*0.01)in(min-d,max+d)|otherwise=(min,max)labelvs=mapfromRational$steps(fromIntegral(la_nLabels_lap))rtickvs=mapfromRational$steps(fromIntegral(la_nTicks_lap))(minimumlabelvs,maximumlabelvs)gridvs=labelvsr=rangepssteps::RealFloata=>a->(a,a)->[Rational]stepsnSteps(min,max)=map((s*).fromIntegral)[min'..max']wheres=chooseStepnSteps(min,max)min'=floor$realToFracmin/smax'=ceiling$realToFracmax/sn=(max'-min')chooseStep::RealFloata=>a->(a,a)->RationalchooseStepnsteps(x1,x2)=minimumBy(comparingproximity)stepswheredelta=x2-x1mult=10^^(floor$log10$delta/nsteps)steps=map(mult*)[0.1,0.2,0.25,0.5,1.0,2.0,2.5,5.0,10,20,25,50]proximityx=abs$delta/realToFracx-nsteps-- | Given a target number of values, and a list of input points,-- find evenly spaced values from the set {1*X, 2*X, 2.5*X, 5*X} (where-- X is some power of ten) that evenly cover the input points.autoSteps::Int->[Double]->[Double]autoStepsnStepsvs=mapfromRational$steps(fromIntegralnSteps)rwhererange[]=(0,1)range_|min==max=(min-0.5,min+0.5)|otherwise=(min,max)(min,max)=(minimumps,maximumps)ps=filterisValidNumbervsr=rangeps----------------------------------------------------------------------defaultLogAxis::(Showa,RealFloata)=>LogAxisParamsadefaultLogAxis=LogAxisParams{loga_labelf_=showD}-- | Generate a log axis automatically, scaled appropriate for the-- input data.autoScaledLogAxis::RealFloata=>LogAxisParamsa->AxisFnaautoScaledLogAxislapps0=makeAxis'(realToFrac.log)(realToFrac.exp)(loga_labelf_lap)(wraprlabelvs,wraprtickvs,wraprgridvs)whereps=filter(\x->isValidNumberx&&0<x)ps0(min,max)=(minimumps,maximumps)wrap=mapfromRationalrange[]=(3,30)range_|min==max=(realToFrac$min/3,realToFrac$max*3)|otherwise=(realToFrac$min,realToFrac$max)(rlabelvs,rtickvs,rgridvs)=logTicks(rangeps)dataLogAxisParamsa=LogAxisParams{-- | The function used to show the axes labels.loga_labelf_::a->String}{-
Rules: Do not subdivide between powers of 10 until all powers of 10
get a major ticks.
Do not subdivide between powers of ten as [1,2,4,6,8,10] when
5 gets a major ticks
(ie the major ticks need to be a subset of the minor tick)
-}logTicks::Range->([Rational],[Rational],[Rational])logTicks(low,high)=(major,minor,major)whereratio=high/lowloweral=let(i,r)=frac(log10a)in(maximum(1:filter(\x->log10(fromRationalx)<=r)l))*10^^iupperal=let(i,r)=properFraction(log10a)in(minimum(10:filter(\x->r<=log10(fromRationalx))l))*10^^ipowers::(Double,Double)->[Rational]->[Rational]powers(x,y)l=[a*10^^p|p<-[(floor(log10x))..(ceiling(log10y))],a<-l]midselectionrl=filter(inRangerl)(powersrl)inRange(a,b)lx=(loweral<=x)&&(x<=upperbl)major|17.5<log10ratio=map(\x->10^^(roundx))$steps(min5(log10ratio))(log10low,log10high)|12<log10ratio=map(\x->10^^(roundx))$steps((log10ratio)/5)(log10low,log10high)|6<log10ratio=map(\x->10^^(roundx))$steps((log10ratio)/2)(log10low,log10high)|3<log10ratio=midselection(low,high)[1,10]|20<ratio=midselection(low,high)[1,5,10]|6<ratio=midselection(low,high)[1,2,4,6,8,10]|3<ratio=midselection(low,high)[1..10]|otherwise=steps5(low,high)(l',h')=(minimummajor,maximummajor)(dl',dh')=(fromRationall',fromRationalh')ratio'=fromRational(h'/l')minor|50<log10ratio'=map(\x->10^^(roundx))$steps50(log10$dl',log10$dh')|6<log10ratio'=filter(\x->l'<=x&&x<=h')$powers(dl',dh')[1,10]|3<log10ratio'=filter(\x->l'<=x&&x<=h')$powers(dl',dh')[1,5,10]|6<ratio'=filter(\x->l'<=x&&x<=h')$powers(dl',dh')[1..10]|3<ratio'=filter(\x->l'<=x&&x<=h')$powers(dl',dh')[1,1.2..10]|otherwise=steps50(dl',dh')log10::(Floatinga)=>a->alog10=logBase10fracx|0<=b=(a,b)|otherwise=(a-1,b+1)where(a,b)=properFractionx$(deriveAccessors''LinearAxisParams)$(deriveAccessors''LogAxisParams)