----------------------------------------------------------------------------- -- | -- 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 #-} module Graphics.Rendering.Chart.Axis.Floating( Percent(..), LinearAxisParams(..), LogValue(..), LogAxisParams(..), defaultLinearAxis, defaultLogAxis, autoScaledAxis, autoScaledLogAxis, autoSteps, la_labelf, la_nLabels, la_nTicks, loga_labelf ) where import Data.List(minimumBy) import Data.Ord (comparing) import Data.Accessor.Template import Graphics.Rendering.Chart.Types import Graphics.Rendering.Chart.Axis.Types instance PlotValue Double where toValue = id fromValue= id autoAxis = autoScaledAxis defaultLinearAxis -- | A wrapper class for doubles used to indicate they are to -- be plotted against a percentage axis. newtype Percent = Percent {unPercent :: Double} deriving (Eq,Ord,Num,Real,Fractional,RealFrac,Floating,RealFloat) instance Show Percent where show (Percent d) = showD (d*100) ++ "%" instance PlotValue Percent where toValue = unPercent fromValue= Percent autoAxis = autoScaledAxis defaultLinearAxis{-la_labelf_=-} -- | A wrapper class for doubles used to indicate they are to -- be plotted against a log axis. newtype LogValue = LogValue Double deriving (Eq, Ord, Num, Real, Fractional, RealFrac, Floating, RealFloat) instance Show LogValue where show (LogValue x) = show x instance PlotValue LogValue where toValue (LogValue x) = log x fromValue d = LogValue (exp d) autoAxis = autoScaledLogAxis defaultLogAxis showD x = case reverse $ show x of '0':'.':r -> reverse r _ -> show x data LinearAxisParams a = 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 :: (Show a) => LinearAxisParams a defaultLinearAxis = LinearAxisParams { la_labelf_ = showD, la_nLabels_ = 5, la_nTicks_ = 50 } -- | Generate a linear axis automatically, scaled appropriately for the -- input data. autoScaledAxis :: RealFloat a => LinearAxisParams a -> AxisFn a autoScaledAxis lap ps0 = makeAxis' realToFrac realToFrac (la_labelf_ lap) (labelvs,tickvs,gridvs) where ps = filter isValidNumber ps0 (min,max) = (minimum ps,maximum ps) range [] = (0,1) range _ | min == max = (min-1,min+1) | otherwise = (min,max) labelvs = map fromRational $ steps (fromIntegral (la_nLabels_ lap)) r tickvs = map fromRational $ steps (fromIntegral (la_nTicks_ lap)) (minimum labelvs,maximum labelvs) gridvs = labelvs r = range ps steps :: RealFloat a => a -> (a,a) -> [Rational] steps nSteps (min,max) = map ((s*) . fromIntegral) [min' .. max'] where s = chooseStep nSteps (min,max) min' = floor $ realToFrac min / s max' = ceiling $ realToFrac max / s n = (max' - min') chooseStep :: RealFloat a => a -> (a,a) -> Rational chooseStep nsteps (x1,x2) = minimumBy (comparing proximity) steps where delta = x2 - x1 mult = 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] proximity x = abs $ delta / realToFrac x - 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] autoSteps nSteps vs = map fromRational $ steps (fromIntegral nSteps) r where range [] = (0,1) range _ | min == max = (min-0.5,min+0.5) | otherwise = (min,max) (min,max) = (minimum ps,maximum ps) ps = filter isValidNumber vs r = range ps ---------------------------------------------------------------------- defaultLogAxis :: Show a => LogAxisParams a defaultLogAxis = LogAxisParams { loga_labelf_ = showD } -- | Generate a log axis automatically, scaled appropriate for the -- input data. autoScaledLogAxis :: RealFloat a => LogAxisParams a -> AxisFn a autoScaledLogAxis lap ps0 = makeAxis' (realToFrac . log) (realToFrac . exp) (loga_labelf_ lap) (wrap rlabelvs, wrap rtickvs, wrap rgridvs) where ps = filter (\x -> isValidNumber x && 0 < x) ps0 (min,max) = (minimum ps,maximum ps) wrap = map fromRational range [] = (3,30) range _ | min == max = (realToFrac $ min/3, realToFrac $ max*3) | otherwise = (realToFrac $ min, realToFrac $ max) (rlabelvs, rtickvs, rgridvs) = logTicks (range ps) data LogAxisParams a = 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) where ratio = high/low lower a l = let (i,r) = frac (log10 a) in (maximum (1:filter (\x -> log10 (fromRational x) <= r) l))*10^^i upper a l = let (i,r) = properFraction (log10 a) in (minimum (10:filter (\x -> r <= log10 (fromRational x)) l))*10^^i powers :: (Double,Double) -> [Rational] -> [Rational] powers (x,y) l = [ a*10^^p | p <- [(floor (log10 x))..(ceiling (log10 y))] , a <- l ] midselection r l = filter (inRange r l) (powers r l) inRange (a,b) l x = (lower a l <= x) && (x <= upper b l) major | 17.5 < log10 ratio = map (\x -> 10^^(round x)) $ steps (min 5 (log10 ratio)) (log10 low, log10 high) | 12 < log10 ratio = map (\x -> 10^^(round x)) $ steps ((log10 ratio)/5) (log10 low, log10 high) | 6 < log10 ratio = map (\x -> 10^^(round x)) $ steps ((log10 ratio)/2) (log10 low, log10 high) | 3 < log10 ratio = 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 = steps 5 (low,high) (l',h') = (minimum major, maximum major) (dl',dh') = (fromRational l', fromRational h') ratio' = fromRational (h'/l') minor | 50 < log10 ratio' = map (\x -> 10^^(round x)) $ steps 50 (log10 $ dl', log10 $ dh') | 6 < log10 ratio' = filter (\x -> l'<=x && x <=h') $ powers (dl', dh') [1,10] | 3 < log10 ratio' = 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 = steps 50 (dl', dh') log10 :: (Floating a) => a -> a log10 = logBase 10 frac x | 0 <= b = (a,b) | otherwise = (a-1,b+1) where (a,b) = properFraction x $( deriveAccessors ''LinearAxisParams ) $( deriveAccessors ''LogAxisParams )