packages feed

Eq-1.1: EqManips/Renderer/Ascii2DGrapher.hs

-- | This module implement an ASCII Art graph plotter,
-- using subdivision to provide good looking ascii graph.
module EqManips.Renderer.Ascii2DGrapher(
                                       -- * Plotting configuration
                                         PlotConf( .. )
                                       , ScalingType( .. )
                                       , Dimension( .. )
                                       , defaultPlotConf
                                       -- * Da Ploting LAUNCHER !!
                                       , plot2DExpression
                                       ) where

import Data.Array.Unboxed
import Text.Printf

import EqManips.Types
import qualified EqManips.Algorithm.StackVM.Stack as VM

-- | Alias in case I want to change in the future.
type ValueType = Double

-- | (Begin, End), all inclusive
type PlotRange = (ValueType, ValueType)

data ScalingType =
      Linear
    | Logarithmic
    deriving Show

data Dimension = Dimension
    { minVal :: ValueType
    , maxVal :: ValueType
    , projectionSize :: Int
    , scaling :: ScalingType
    , drawAxis :: Bool
    , labelPrecision :: Int
    , labelEvery :: Maybe Int
    }
    deriving Show

data PlotConf = PlotConf
    { xDim :: Dimension
    , yDim :: Dimension
    , draw0Axis :: Bool
    , graphTitle :: Maybe String
    }
    deriving Show

defaultPlotConf :: PlotConf
defaultPlotConf = PlotConf
    { xDim = Dimension
        { minVal = 0.0
        , maxVal = 10.0
        , projectionSize = 50
        , scaling = Linear
        , drawAxis = False
        , labelPrecision = 4
        , labelEvery = Just 7
        }

    , yDim = Dimension
        { minVal = -5.0
        , maxVal = 5.0
        , projectionSize = 30
        , scaling = Linear
        , drawAxis = False
        , labelPrecision = 4
        , labelEvery = Just 4
        }

    , draw0Axis = False
    , graphTitle = Nothing
    }

doubleShow :: Dimension -> ValueType -> String
doubleShow dim = printf "%.*f" (labelPrecision dim)

dimensionRange :: Dimension -> PlotRange
dimensionRange dim = (minVal dim, maxVal dim)

canvasSize :: PlotConf -> (Int, Int)
canvasSize conf = ( projectionSize $ xDim conf
                  , projectionSize $ yDim conf)

-- | Translate a list of write on the x (width) axis with
-- a given amount. Perform no operation if translation amount
-- is 0.
translateX :: Int -> [((Int, Int), Char)] -> [((Int, Int), Char)]
translateX 0 lst = lst
translateX i lst = [ ((x + i, y), c) | ((x,y), c) <- lst ]

-- | Same thing as 'translateX' but with the y (height) axis.
translateY :: Int -> [((Int, Int), Char)] -> [((Int, Int), Char)]
translateY 0 lst = lst
translateY i lst = [ ((x, y + i), c) | ((x,y), c) <- lst ]

-- | Add some vertical labels
addYAxisLabel :: Dimension -> ValSuccessor -> CharCanvas -> CharCanvas
addYAxisLabel dim successor rez@(((xPos, shiftHeight), adds), vals) =
 case (drawAxis dim, labelEvery dim) of
  (_, Nothing) -> rez
  (False, _) -> rez
  (True, Just size) ->
   (((xShift, shiftHeight), adds), vals' ++ draw shiftHeight (minVal dim))
    where maxHeight = projectionSize dim + shiftHeight

          xShift = max 8 xPos
          vals' = translateX (xShift - xPos) vals
          
          apply val 0 = val
          apply val times = apply (successor val) $ times - 1

          draw y yVal
            | y >= maxHeight = []
            | otherwise = 
                let indicator = ((xShift - 1, y), '+')
                    future = draw (y + size) (apply yVal size)
                in indicator :
                    [((xP, y), c) | (xP, c) <- zip [0.. xShift - 2] 
                                            $ doubleShow dim yVal] ++
                                    future

-- | Represent a tuple of canvas extension and a list
-- of characters. It's ((leftAdd, bottomAdd), (rightAdd, topAdd))
type CharCanvas =
    (((Int,Int),(Int,Int)), [((Int,Int), Char)])

addXAxisLabel :: Dimension -> ValSuccessor -> CharCanvas -> CharCanvas
addXAxisLabel dim successor rez@(((shiftWidth, yPos), (addX, addY)), vals) = 
 case (drawAxis dim, labelEvery dim) of
  (_, Nothing) -> rez
  (False, _) -> rez
  (True, Just size) ->
   (((shiftWidth, yPos)
   ,(rightShift, addY) ), vals ++ draw shiftWidth (minVal dim))
    where maxWidth = projectionSize dim + shiftWidth

          apply val 0 = val
          apply val times = apply (successor val) $ times - 1

          rightShift = max addX 
                     $ size - (projectionSize dim `rem` size)

          draw x xVal
            | x >= maxWidth = []
            | otherwise = 
                let indicator = ((x - 1,1), '|')
                    future = draw (x + size) (apply xVal size)
                in indicator : [((xPos, 0), c)
                                    | (xPos, c) <- zip [x - 1.. x + size - 3] 
                                                    $ doubleShow dim xVal] ++ future
                
addTitle :: PlotConf -> Maybe String -> CharCanvas -> CharCanvas
addTitle _ Nothing a = a
addTitle conf (Just t) (((shiftWidth, shiftHeight), adds), vals) =
    (((shiftWidth, shiftHeight + 2), adds), toAdd ++ translateY 2 vals)
        where begin = (projectionSize (xDim conf) - length t) `div` 2
              toAdd = [((x,0), c) | (x,c) <- zip [begin ..] t]

add0Axis :: PlotConf -> Scaler -> CharCanvas -> CharCanvas 
add0Axis conf scaler original@(((shiftWidth, shiftHeight), adds), vals) =
    if y < 0 then original else
    ( ((wShift, shiftHeight), adds)
    , ((wShift - nominalShift + 1, y), '0') : 
        line ++ translateX valShift vals)
    where w = projectionSize $ xDim conf
          h = projectionSize $ yDim conf
          y = scaler 0
          line = if y >= 0 && y < h
            then [((x, y), '-') | 
                    x <- [wShift .. wShift + (w - 1)]]
            else []
          nominalShift = 4
          wShift = max nominalShift shiftWidth
          valShift = if shiftWidth >= nominalShift
            then shiftWidth - wShift
            else wShift - shiftWidth

addYAxis :: PlotConf -> Scaler -> CharCanvas -> CharCanvas
addYAxis conf _scaler (((shiftWidth, shiftHeight), adds), vals) =
    ( ((wShift, shiftHeight), adds)
    ,  line ++ translateX valShift vals)
    where h = projectionSize $ yDim conf
          x = nominalShift - 1
          line = [((x, y), '|') | 
                    y <- [shiftHeight .. shiftHeight + (h - 1)]]
          nominalShift = 4
          wShift = max nominalShift shiftWidth
          valShift = if shiftWidth >= nominalShift
            then shiftWidth - wShift
            else wShift - shiftWidth


addXaxis :: PlotConf -> Scaler -> CharCanvas -> CharCanvas
addXaxis conf _ (((shiftWidth, shiftHeight), adds), vals) =
  ( ((shiftWidth, hShift), adds)
  , line ++ translateY valShift vals)
    where line = [((x, hShift - 1), '_') 
                        | x <- [shiftWidth ..(w - 1) + shiftWidth]]
          w = projectionSize $ xDim conf
          nominalShift = 2
          hShift = max nominalShift shiftHeight
          valShift = hShift - shiftHeight

-- | Equivalent of 'when' but non-monadic.
doWhen :: Bool -> (a -> a) -> a -> a
doWhen False _ a = a
doWhen True  f a = f a

-- | Function in charge of adding all the plot axis
-- to the generated character stream
addAxis :: PlotConf
        -> (Scaler, Scaler)
        -> (ValSuccessor, ValSuccessor)
        -> [((Int, Int), Char)]
        -> CharCanvas
addAxis conf (widthScaler, heightScaler) (xSucc, ySucc) a = 
      doWhen (graphTitle conf /= Nothing)
             (addTitle conf $ graphTitle conf)
    . doWhen (labelEvery (yDim conf) /= Nothing)
             (addYAxisLabel (yDim conf) ySucc)
    . doWhen (drawAxis $ yDim conf)
             (addYAxis conf heightScaler)
    . doWhen (labelEvery (xDim conf) /= Nothing)
             (addXAxisLabel (xDim conf) xSucc)
    . doWhen (drawAxis $ xDim conf)
             (addXaxis conf widthScaler)
    . doWhen (draw0Axis conf)
             (add0Axis conf heightScaler) $ (((0,0), (0,0)), a)


-- | User function to start a plot. Handle all the scary
-- configuration before starting the plot.
plot2DExpression :: PlotConf -> FormulaPrim
                 -> Either String (UArray (Int, Int) Char)
plot2DExpression conf formula =
    case VM.compileExpression formula of
      Left err -> Left err
      Right prog ->
        let successor = widthSuccessor $ xDim conf
            (_,ySuccessor) = widthSuccessor $ yDim conf
            yScaler = sizeMapper $ yDim conf
            xScaler = sizeMapper $ xDim conf
            (xBegin, xEnd) = dimensionRange $ xDim conf
            size@(w, h)  = canvasSize conf
            graph = plot2D size xEnd
                           (flip (VM.evalProgram prog) 0)
                           successor xScaler yScaler
                           xBegin
            (((shiftX, shiftY), (addX, addY)), graph') =
                addAxis conf (xScaler, yScaler) (snd successor, ySuccessor) graph
        in Right $ accumArray (\_ e -> e) ' '
                              ((0, 0) ,(w + shiftX + addX - 1, h + shiftY + addY - 1)) $
                              [v | v@((x,_),_) <- graph', 
                                                 x < w + shiftX + addX,
                                                 x >= 0]


-- | This type is a transformation from function
-- result to screen space.
type Scaler = ValueType -> Int

-- | Function used to find the next \'x\' element
-- to be plotted.
type ValSuccessor =
    ValueType -> ValueType

-- | Equivalent of the 'succ' function of the
-- 'Enum' class, with a linear scale.
widthSuccessor :: Dimension -> (ValSuccessor, ValSuccessor)
widthSuccessor dim = case (scaling dim, minVal dim > 0) of
  (Linear, _) -> (\v -> v - addVal, \v -> v + addVal)
    where addVal = (vMax - vMin) / toEnum (projectionSize dim - 2)
          (vMin, vMax) = dimensionRange dim
  (Logarithmic, True)  -> (\v -> v / mulVal,\v -> v * mulVal)
    where mulVal = (vMax / vMin) ** (1.0 / toEnum (projectionSize dim - 1))
          (vMin, vMax) = dimensionRange dim
  (Logarithmic, False) -> (\v -> vPrev (v + vAdd) - vAdd
                          ,\v -> vNext (v + vAdd) - vAdd)
    where (vMin, vMax) = dimensionRange dim
          bigpsilon = 0.1
          vAdd = 0.1 + negate vMin
          (vPrev, vNext) = widthSuccessor $ 
                dim { minVal = bigpsilon
                    , maxVal = vMax - vMin + bigpsilon}
          


-- | How to map the height value onto the screen,
-- by taking tinto action the 'canvas' size
sizeMapper :: Dimension -> (ValueType -> Int)
sizeMapper dim = 
 let (vMin, vMax) = dimensionRange dim
     fullSize = projectionSize dim
 in case (scaling dim, vMin > 0) of
   (Linear, _) -> \val -> truncate $ (val - vMin) * scaler
      where scaler = toEnum fullSize / (vMax - vMin + 1)

   (Logarithmic, True) -> \val -> truncate $ (log val - vMin') * scaler
      where (vMin', vMax') = (log vMin, log vMax)
            scaler = toEnum fullSize / (abs (vMax' - vMin') + 1)

   (Logarithmic, False) -> \val -> truncate $ (log $ val - vMin') * scaler
      where (vMin', vMax') = (log 0.1, log $ vMax - vMin)
            scaler = toEnum fullSize / (abs (vMax' - vMin') + 1)

              
-- | Describe the action that the plotter must
-- accomplish in order to draw a function
data DrawAction =
    ActionStop          -- ^ Stop the ploting/subdivision for this value
  | SubdivideBoth  Char -- ^ Halve the x interval and continue plotting, on both ends
  | SubdivideUpper Char -- ^ Halve and continue only on the upper part.
  | SubdivideLower Char -- ^ Halve and continue only on the lower part.
  | SubdivideIgnore     -- ^ Halve and continue both ends but don't write any char.
  | Continue Char       -- ^ Continue with the current interval, adn write a char.

neighbour :: ValueType -> ValueType -> Bool
neighbour y1 y2 = abs (y1 - y2) < 0.05

-- | Given a successor function given as parameter,
-- it will return a successor function going half
-- as far as the previous one. Work with backward
-- functions to.
rangeSplitter :: ValSuccessor -> ValSuccessor
rangeSplitter f x = x + (f x - x) / 2

-- | As side is inversed when drawing backward,
-- this function help to choose a representation
-- given the current direction and a 'Forward'
-- assention or 'Backward' descent.
sideChar :: Direction           -- ^ Current drawing direction
         -> Direction          -- ^ Assention or descent
         -> Char
sideChar Forward Forward = '/'
sideChar Forward Backward = '\\'
sideChar Backward a = sideChar Forward $ inverseDirection a

-- | Given two samples, give an Ascii representation
-- and information to the plotter on how to continue
-- the drawing.
charOf :: Direction        -- ^ Current plotting direction
       -> Int              -- ^ Canvas height
       -> Int              -- ^ Absciss in canvas space of the previous value.
       -> (ValueType, Int) -- ^ Value and canvas position of the current value.
       -> (ValueType, Int) -- ^ Value and canvas position of the current value.
       -> DrawAction       -- ^ What to do next
charOf direction height screenPrev (y1, screenY1) (y2, screenY2)
   | isNaN y1 = ActionStop
   | isInfinite y1 && screenY1 >= 0 && screenY1 < height =
       SubdivideBoth '|'
   | isInfinite y1 = SubdivideIgnore
   -- We are out of the drawing box, stop
   -- the drawing for the current value of x
   | screenY1 >= height || screenY1 < 0 = ActionStop
  
  
   -- The two values are in a different cell,
   -- we need to refine the values.
   | abs (screenY1 - screenY2) > 1 && abs (screenY1 - screenPrev) > 1
       = SubdivideBoth '|'
  
   | abs (screenY1 - screenY2) > 1 = SubdivideUpper '|'
  
   | abs (screenY1 - screenPrev) > 1 = SubdivideLower '|'
  
   -- If values are sufisently near, draw a flat
   -- line and continue
   | neighbour y1 y2 = Continue '-'
  
   -- We are ascending, but not enough to subdivide,
   -- continue to the next x
   | y1 < y2 = Continue $ sideChar direction Forward
  
   -- Descending...
   | y1 > y2 =  Continue $ sideChar direction Backward
  
   -- y1 more or less equal y2
   | otherwise = Continue '-'


-- | Happy float
epsilon :: ValueType
epsilon = 0.00000000000001

-- | Type used when plotting, to inform
-- the subdivision direction.
data Direction = Forward | Backward
    deriving Eq

-- | Inverse the direction, equivalent of
-- 'not', but for 'Direction'
inverseDirection :: Direction -> Direction
inverseDirection Forward = Backward
inverseDirection Backward = Forward

-- | The real plotting function, calling it is rather complex,
-- due to the number of thing to take into account, favor the use
-- of a more high level function like 'plot2DExpression'
plot2D :: (Int, Int)              -- ^ Size of the canvas in number of cells
       -> ValueType               -- ^ End value for x
       -> (ValueType -> ValueType) -- ^ The function to be evaluated
       -> (ValSuccessor, ValSuccessor)  -- ^ x Successor function, backward, forward,
       -> Scaler                  -- ^ Function to translate xVal to canvas position
       -> Scaler                  -- ^ Function to translate (f xVal) to canvas position
       -> ValueType    -- ^ The \'current\' ploted value, xBegin for first call
       -> [((Int, Int),Char)] -- ^ Woohoo, the result, to be stored in an array
plot2D (_width, height) xStop f widthSucc xPlot yPlot xInit = 
 subPlot widthSucc (xInit - epsilon, xStop) Forward 0 xInit
  where subPlot successors@(xPrev, xSucc)
                interval@(xBegin, xEnd) 
                direction prevScreen x
          | direction == Forward && (x <= xBegin || x >= xEnd) = []
          | direction == Backward && (x <= xEnd || x >= xBegin) = []
          | otherwise =
          let val = f x
              xNext = if direction == Forward then xSucc x
                                             else xPrev x
              screenY = yPlot val
              midPoint = (x + xNext) / 2
              halfSuccessors@(halfPrev, halfSucc) =
                  (rangeSplitter $ rangeSplitter xPrev
                  ,rangeSplitter $ rangeSplitter xSucc)

              (subPrev, subSucc) = if direction == Forward
                    then (halfPrev, halfSucc)
                    else (halfSucc, halfPrev)
              midInfo = yPlot $ f midPoint

              lowerRange = subPlot halfSuccessors 
                                   (midPoint, xBegin)
                                   (inverseDirection direction)
                                   midInfo 
                                   $ subPrev midPoint

              upperRange = subPlot halfSuccessors
                                   (midPoint, xNext) 
                                   direction
                                   midInfo
                                   $ subSucc  midPoint

              midChar = if midInfo > 0 && midInfo < height
                    then [((xPlot midPoint, midInfo), '|')]
                    else []
              future = subPlot successors interval direction
                               screenY xNext


          in case charOf direction height prevScreen
                         (val, screenY) (f xNext, yPlot $ f xNext) of 
            ActionStop -> future
            Continue c -> ((xPlot x, screenY), c) : future

            SubdivideLower c ->
                lowerRange ++ midChar ++ ((xPlot x, screenY),c) : future
            SubdivideUpper c ->
                upperRange ++ midChar ++ ((xPlot x, screenY),c) : future
            SubdivideBoth c ->
                lowerRange ++ upperRange ++
                    midChar ++ ((xPlot x, screenY),c) : future
            SubdivideIgnore ->
                lowerRange ++ upperRange ++ midChar ++ future